Merge pull request #3456 from vector-im/feature/fga/dial_pad_tab

Feature/fga/dial pad tab
This commit is contained in:
Benoit Marty 2021-06-11 12:23:19 +02:00 committed by GitHub
commit dfb01a462e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 290 additions and 193 deletions

View File

@ -51,7 +51,7 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
} }
} }
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent { fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) { return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) {
DaggerSessionComponent DaggerSessionComponent
.factory() .factory()

View File

@ -31,9 +31,11 @@ internal class DirectChatsHelper @Inject constructor(@SessionDatabase
*/ */
fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> { fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> {
return Realm.getInstance(realmConfiguration).use { realm -> return Realm.getInstance(realmConfiguration).use { realm ->
// Makes sure we have the latest realm updates, this is important as we sent this information to the server.
realm.refresh()
RoomSummaryEntity.getDirectRooms(realm) RoomSummaryEntity.getDirectRooms(realm)
.asSequence() .asSequence()
.filter { it.roomId != filterRoomId && it.directUserId != null } .filter { it.roomId != filterRoomId && it.directUserId != null && it.membership.isActive() }
.groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId }) .groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId })
} }
} }

1
newsfragment/3333.bugfix Normal file
View File

@ -0,0 +1 @@
Fix new DMs not always marked as such

1
newsfragment/3457.misc Normal file
View File

@ -0,0 +1 @@
Move the ability to start a call from dialpad directly to a dedicated tab in the home screen.

View File

@ -116,7 +116,9 @@ class DefaultErrorFormatter @Inject constructor(
throwable.localizedMessage throwable.localizedMessage
} }
} }
is DialPadLookup.Failure -> is DialPadLookup.Failure.NumberIsYours ->
stringProvider.getString(R.string.cannot_call_yourself)
is DialPadLookup.Failure.NoResult ->
stringProvider.getString(R.string.call_dial_pad_lookup_error) stringProvider.getString(R.string.call_dial_pad_lookup_error)
else -> throwable.localizedMessage else -> throwable.localizedMessage
} }

View File

@ -1,46 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetCallDialerChoiceBinding
class DialerChoiceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialerChoiceBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallDialerChoiceBinding {
return BottomSheetCallDialerChoiceBinding.inflate(inflater, container, false)
}
var onDialPadClicked: (() -> Unit)? = null
var onVoiceCallClicked: (() -> Unit)? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.dialerChoiceDialPad.views.bottomSheetActionClickableZone.debouncedClicks {
onDialPadClicked?.invoke()
dismiss()
}
views.dialerChoiceVoiceCall.views.bottomSheetActionClickableZone.debouncedClicks {
onVoiceCallClicked?.invoke()
dismiss()
}
}
}

View File

@ -26,9 +26,9 @@ import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.android.dialer.dialpadview.DialpadView import com.android.dialer.dialpadview.DialpadView
import com.android.dialer.dialpadview.DigitsEditText import com.android.dialer.dialpadview.DigitsEditText
import com.android.dialer.dialpadview.R
import com.google.i18n.phonenumbers.AsYouTypeFormatter import com.google.i18n.phonenumbers.AsYouTypeFormatter
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import im.vector.app.R
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
class DialPadFragment : Fragment() { class DialPadFragment : Fragment() {
@ -57,7 +57,7 @@ class DialPadFragment : Fragment() {
dialpadView.findViewById<View>(R.id.dialpad_key_voicemail).isVisible = false dialpadView.findViewById<View>(R.id.dialpad_key_voicemail).isVisible = false
digits = dialpadView.digits as? DigitsEditText digits = dialpadView.digits as? DigitsEditText
digits?.isCursorVisible = cursorVisible digits?.isCursorVisible = cursorVisible
digits?.setTextColor(ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.vctr_content_primary)) digits?.setTextColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_content_primary))
dialpadView.findViewById<View>(R.id.zero).setOnClickListener { append('0') } dialpadView.findViewById<View>(R.id.zero).setOnClickListener { append('0') }
if (enablePlus) { if (enablePlus) {
dialpadView.findViewById<View>(R.id.zero).setOnLongClickListener { dialpadView.findViewById<View>(R.id.zero).setOnLongClickListener {

View File

@ -17,10 +17,11 @@
package im.vector.app.features.call.dialpad package im.vector.app.features.call.dialpad
import im.vector.app.features.call.lookup.pstnLookup import im.vector.app.features.call.lookup.pstnLookup
import im.vector.app.features.call.lookup.sipNativeLookup
import im.vector.app.features.call.vectorCallService
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.createdirect.DirectRoomHelper
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import java.lang.IllegalStateException
import javax.inject.Inject import javax.inject.Inject
class DialPadLookup @Inject constructor( class DialPadLookup @Inject constructor(
@ -28,13 +29,25 @@ class DialPadLookup @Inject constructor(
private val webRtcCallManager: WebRtcCallManager, private val webRtcCallManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper private val directRoomHelper: DirectRoomHelper
) { ) {
class Failure : Throwable() sealed class Failure : Throwable() {
object NoResult: Failure()
object NumberIsYours: Failure()
}
data class Result(val userId: String, val roomId: String) data class Result(val userId: String, val roomId: String)
suspend fun lookupPhoneNumber(phoneNumber: String): Result { suspend fun lookupPhoneNumber(phoneNumber: String): Result {
val thirdPartyUser = session.pstnLookup(phoneNumber, webRtcCallManager.supportedPSTNProtocol).firstOrNull() ?: throw IllegalStateException() session.vectorCallService.protocolChecker.awaitCheckProtocols()
val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId) val thirdPartyUser = session.pstnLookup(phoneNumber, webRtcCallManager.supportedPSTNProtocol).firstOrNull() ?: throw Failure.NoResult
return Result(userId = thirdPartyUser.userId, roomId = roomId) // check to see if this is a virtual user, in which case we should find the native user
val nativeUserId = if (webRtcCallManager.supportsVirtualRooms) {
val nativeLookupResults = session.sipNativeLookup(thirdPartyUser.userId)
nativeLookupResults.firstOrNull()?.userId ?: thirdPartyUser.userId
} else {
thirdPartyUser.userId
}
if (nativeUserId == session.myUserId) throw Failure.NumberIsYours
val roomId = directRoomHelper.ensureDMExists(nativeUserId)
return Result(userId = nativeUserId, roomId = roomId)
} }
} }

View File

@ -16,10 +16,13 @@
package im.vector.app.features.call.lookup package im.vector.app.features.call.lookup
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
private const val LOOKUP_SUCCESS_FIELD = "lookup_success"
suspend fun Session.pstnLookup(phoneNumber: String, protocol: String?): List<ThirdPartyUser> { suspend fun Session.pstnLookup(phoneNumber: String, protocol: String?): List<ThirdPartyUser> {
if (protocol == null) return emptyList() if (protocol == null) return emptyList()
return tryOrNull { return tryOrNull {
@ -36,7 +39,11 @@ suspend fun Session.sipVirtualLookup(nativeMxid: String): List<ThirdPartyUser> {
protocol = PROTOCOL_SIP_VIRTUAL, protocol = PROTOCOL_SIP_VIRTUAL,
fields = mapOf("native_mxid" to nativeMxid) fields = mapOf("native_mxid" to nativeMxid)
) )
}.orEmpty() }
.orEmpty()
.filter {
(it.fields[LOOKUP_SUCCESS_FIELD] as? Boolean).orFalse()
}
} }
suspend fun Session.sipNativeLookup(virtualMxid: String): List<ThirdPartyUser> { suspend fun Session.sipNativeLookup(virtualMxid: String): List<ThirdPartyUser> {
@ -45,5 +52,9 @@ suspend fun Session.sipNativeLookup(virtualMxid: String): List<ThirdPartyUser> {
protocol = PROTOCOL_SIP_NATIVE, protocol = PROTOCOL_SIP_NATIVE,
fields = mapOf("virtual_mxid" to virtualMxid) fields = mapOf("virtual_mxid" to virtualMxid)
) )
}.orEmpty() }
.orEmpty()
.filter {
(it.fields[LOOKUP_SUCCESS_FIELD] as? Boolean).orFalse()
}
} }

View File

@ -19,6 +19,7 @@ package im.vector.app.features.home
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class HomeDetailAction : VectorViewModelAction { sealed class HomeDetailAction : VectorViewModelAction {
data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction() data class SwitchTab(val tab: HomeTab) : HomeDetailAction()
object MarkAllRoomsRead : HomeDetailAction() object MarkAllRoomsRead : HomeDetailAction()
data class StartCallWithPhoneNumber(val phoneNumber: String): HomeDetailAction()
} }

View File

@ -23,6 +23,8 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.iterator
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -41,12 +43,14 @@ import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.DialPadFragment
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.RoomListParams
import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -101,6 +105,9 @@ class HomeDetailFragment @Inject constructor(
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) { state ->
menu.iterator().forEach { it.isVisible = state.currentTab is HomeTab.RoomList }
}
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
} }
@ -123,7 +130,7 @@ class HomeDetailFragment @Inject constructor(
withState(viewModel) { withState(viewModel) {
// Update the navigation view if needed (for when we restore the tabs) // Update the navigation view if needed (for when we restore the tabs)
views.bottomNavigationView.selectedItemId = it.displayMode.toMenuId() views.bottomNavigationView.selectedItemId = it.currentTab.toMenuId()
} }
viewModel.selectSubscribe(this, HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod -> viewModel.selectSubscribe(this, HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod ->
@ -137,8 +144,20 @@ class HomeDetailFragment @Inject constructor(
} }
} }
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> viewModel.selectSubscribe(this, HomeDetailViewState::currentTab) { currentTab ->
switchDisplayMode(displayMode) updateUIForTab(currentTab)
}
viewModel.selectSubscribe(this, HomeDetailViewState::showDialPadTab) { showDialPadTab ->
updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab)
}
viewModel.observeViewEvents { viewEvent ->
when (viewEvent) {
HomeDetailViewEvents.CallStarted -> dismissLoadingDialog()
is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure)
HomeDetailViewEvents.Loading -> showLoadingDialog()
}
} }
unknownDeviceDetectorSharedViewModel.subscribe { state -> unknownDeviceDetectorSharedViewModel.subscribe { state ->
@ -179,20 +198,8 @@ class HomeDetailFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// update notification tab if needed // update notification tab if needed
checkNotificationTabStatus() updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
} callManager.checkForProtocolsSupportIfNeeded()
private fun checkNotificationTabStatus() {
val wasVisible = views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible
views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
if (wasVisible && !vectorPreferences.labAddNotificationTab()) {
// As we hide it check if it's not the current item!
withState(viewModel) {
if (it.displayMode.toMenuId() == R.id.bottom_action_notification) {
viewModel.handle(HomeDetailAction.SwitchDisplayMode(RoomListDisplayMode.PEOPLE))
}
}
}
} }
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) { private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
@ -321,12 +328,13 @@ class HomeDetailFragment @Inject constructor(
private fun setupBottomNavigationView() { private fun setupBottomNavigationView() {
views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab() views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
views.bottomNavigationView.setOnNavigationItemSelectedListener { views.bottomNavigationView.setOnNavigationItemSelectedListener {
val displayMode = when (it.itemId) { val tab = when (it.itemId) {
R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE R.id.bottom_action_people -> HomeTab.RoomList(RoomListDisplayMode.PEOPLE)
R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS R.id.bottom_action_rooms -> HomeTab.RoomList(RoomListDisplayMode.ROOMS)
else -> RoomListDisplayMode.NOTIFICATIONS R.id.bottom_action_notification -> HomeTab.RoomList(RoomListDisplayMode.NOTIFICATIONS)
else -> HomeTab.DialPad
} }
viewModel.handle(HomeDetailAction.SwitchDisplayMode(displayMode)) viewModel.handle(HomeDetailAction.SwitchTab(tab))
true true
} }
@ -342,13 +350,14 @@ class HomeDetailFragment @Inject constructor(
// } // }
} }
private fun switchDisplayMode(displayMode: RoomListDisplayMode) { private fun updateUIForTab(tab: HomeTab) {
views.groupToolbarTitleView.setText(displayMode.titleRes) views.groupToolbarTitleView.setText(tab.titleRes)
updateSelectedFragment(displayMode) updateSelectedFragment(tab)
invalidateOptionsMenu()
} }
private fun updateSelectedFragment(displayMode: RoomListDisplayMode) { private fun updateSelectedFragment(tab: HomeTab) {
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}" val fragmentTag = "FRAGMENT_TAG_$tab"
val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag) val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag)
childFragmentManager.commitTransaction { childFragmentManager.commitTransaction {
childFragmentManager.fragments childFragmentManager.fragments
@ -357,14 +366,49 @@ class HomeDetailFragment @Inject constructor(
detach(it) detach(it)
} }
if (fragmentToShow == null) { if (fragmentToShow == null) {
val params = RoomListParams(displayMode) when (tab) {
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag) is HomeTab.RoomList -> {
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
}
is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment())
}
}
} else { } else {
if (tab is HomeTab.DialPad) {
(fragmentToShow as? DialPadFragment)?.applyCallback()
}
attach(fragmentToShow) attach(fragmentToShow)
} }
} }
} }
private fun createDialPadFragment(): Fragment {
val fragment = childFragmentManager.fragmentFactory.instantiate(vectorBaseActivity.classLoader, DialPadFragment::class.java.name)
return (fragment as DialPadFragment).apply {
arguments = Bundle().apply {
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
}
applyCallback()
}
}
private fun updateTabVisibilitySafely(tabId: Int, isVisible: Boolean) {
val wasVisible = views.bottomNavigationView.menu.findItem(tabId).isVisible
views.bottomNavigationView.menu.findItem(tabId).isVisible = isVisible
if (wasVisible && !isVisible) {
// As we hide it check if it's not the current item!
withState(viewModel) {
if (it.currentTab.toMenuId() == tabId) {
viewModel.handle(HomeDetailAction.SwitchTab(HomeTab.RoomList(RoomListDisplayMode.PEOPLE)))
}
}
}
}
/* ========================================================================================== /* ==========================================================================================
* KeysBackupBanner Listener * KeysBackupBanner Listener
* ========================================================================================== */ * ========================================================================================== */
@ -399,10 +443,13 @@ class HomeDetailFragment @Inject constructor(
} }
} }
private fun RoomListDisplayMode.toMenuId() = when (this) { private fun HomeTab.toMenuId() = when (this) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people is HomeTab.DialPad -> R.id.bottom_action_dial_pad
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms is HomeTab.RoomList -> when (displayMode) {
else -> R.id.bottom_action_notification RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_notification
}
} }
override fun onTapToReturnToCall() { override fun onTapToReturnToCall() {
@ -421,6 +468,16 @@ class HomeDetailFragment @Inject constructor(
} }
} }
private fun DialPadFragment.applyCallback(): DialPadFragment {
callback = object : DialPadFragment.Callback {
override fun onOkClicked(formatted: String?, raw: String?) {
if (raw.isNullOrEmpty()) return
viewModel.handle(HomeDetailAction.StartCallWithPhoneNumber(raw))
}
}
return this
}
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
return serverBackupStatusViewModelFactory.create(initialState) return serverBackupStatusViewModelFactory.create(initialState)
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home
import im.vector.app.core.platform.VectorViewEvents
sealed class HomeDetailViewEvents : VectorViewEvents {
object Loading : HomeDetailViewEvents()
object CallStarted : HomeDetailViewEvents()
data class FailToCall(val failure: Throwable) : HomeDetailViewEvents()
}

View File

@ -26,8 +26,11 @@ import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler import im.vector.app.AppStateHandler
import im.vector.app.RoomGroupingMethod import im.vector.app.RoomGroupingMethod
import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.di.HasScreenInjector
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.lookup.CallProtocolsChecker
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -51,8 +54,11 @@ import java.util.concurrent.TimeUnit
class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState,
private val session: Session, private val session: Session,
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val callManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper,
private val appStateHandler: AppStateHandler) private val appStateHandler: AppStateHandler)
: VectorViewModel<HomeDetailViewState, HomeDetailAction, EmptyViewEvents>(initialState) { : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
CallProtocolsChecker.Listener {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -64,7 +70,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? { override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? {
val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository() val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository()
return HomeDetailViewState( return HomeDetailViewState(
displayMode = uiStateRepository.getDisplayMode() currentTab = HomeTab.RoomList(uiStateRepository.getDisplayMode())
) )
} }
@ -79,7 +85,8 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
observeSyncState() observeSyncState()
observeRoomGroupingMethod() observeRoomGroupingMethod()
observeRoomSummaries() observeRoomSummaries()
updateShowDialPadTab()
callManager.addProtocolsCheckerListener(this)
session.rx().liveUser(session.myUserId).execute { session.rx().liveUser(session.myUserId).execute {
copy( copy(
myMatrixItem = it.invoke()?.getOrNull()?.toMatrixItem() myMatrixItem = it.invoke()?.getOrNull()?.toMatrixItem()
@ -89,18 +96,48 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
override fun handle(action: HomeDetailAction) { override fun handle(action: HomeDetailAction) {
when (action) { when (action) {
is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action) is HomeDetailAction.SwitchTab -> handleSwitchTab(action)
HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
is HomeDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
} }
} }
private fun handleSwitchDisplayMode(action: HomeDetailAction.SwitchDisplayMode) = withState { state -> private fun handleStartCallWithPhoneNumber(action: HomeDetailAction.StartCallWithPhoneNumber) {
if (state.displayMode != action.displayMode) { viewModelScope.launch {
setState { try {
copy(displayMode = action.displayMode) _viewEvents.post(HomeDetailViewEvents.Loading)
val result = DialPadLookup(session, callManager, directRoomHelper).lookupPhoneNumber(action.phoneNumber)
callManager.startOutgoingCall(result.roomId, result.userId, isVideoCall = false)
_viewEvents.post(HomeDetailViewEvents.CallStarted)
} catch (failure: Throwable) {
_viewEvents.post(HomeDetailViewEvents.FailToCall(failure))
} }
}
}
uiStateRepository.storeDisplayMode(action.displayMode) private fun handleSwitchTab(action: HomeDetailAction.SwitchTab) = withState { state ->
if (state.currentTab != action.tab) {
setState {
copy(currentTab = action.tab)
}
if (action.tab is HomeTab.RoomList) {
uiStateRepository.storeDisplayMode(action.tab.displayMode)
}
}
}
override fun onCleared() {
super.onCleared()
callManager.removeProtocolsCheckerListener(this)
}
override fun onPSTNSupportUpdated() {
updateShowDialPadTab()
}
private fun updateShowDialPadTab() {
setState {
copy(showDialPadTab = callManager.supportsPSTNProtocol)
} }
} }
@ -138,11 +175,11 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
private fun observeRoomGroupingMethod() { private fun observeRoomGroupingMethod() {
appStateHandler.selectedRoomGroupingObservable appStateHandler.selectedRoomGroupingObservable
.subscribe { .subscribe {
setState { setState {
copy( copy(
roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null) roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null)
) )
} }
} }
.disposeOnClear() .disposeOnClear()
} }
@ -165,7 +202,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
is RoomGroupingMethod.ByLegacyGroup -> { is RoomGroupingMethod.ByLegacyGroup -> {
// TODO!! // TODO!!
} }
is RoomGroupingMethod.BySpace -> { is RoomGroupingMethod.BySpace -> {
val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId
val dmInvites = session.getRoomSummaries( val dmInvites = session.getRoomSummaries(
roomSummaryQueryParams { roomSummaryQueryParams {

View File

@ -16,9 +16,11 @@
package im.vector.app.features.home package im.vector.app.features.home
import androidx.annotation.StringRes
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.RoomGroupingMethod import im.vector.app.RoomGroupingMethod
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.SyncState
@ -28,7 +30,7 @@ data class HomeDetailViewState(
val roomGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null), val roomGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null),
val myMatrixItem: MatrixItem? = null, val myMatrixItem: MatrixItem? = null,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized, val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE, val currentTab: HomeTab = HomeTab.RoomList(RoomListDisplayMode.PEOPLE),
val notificationCountCatchup: Int = 0, val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false, val notificationHighlightCatchup: Boolean = false,
val notificationCountPeople: Int = 0, val notificationCountPeople: Int = 0,
@ -36,5 +38,11 @@ data class HomeDetailViewState(
val notificationCountRooms: Int = 0, val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false, val notificationHighlightRooms: Boolean = false,
val hasUnreadMessages: Boolean = false, val hasUnreadMessages: Boolean = false,
val syncState: SyncState = SyncState.Idle val syncState: SyncState = SyncState.Idle,
val showDialPadTab: Boolean = false
) : MvRxState ) : MvRxState
sealed class HomeTab(@StringRes val titleRes: Int) {
data class RoomList(val displayMode: RoomListDisplayMode) : HomeTab(displayMode.titleRes)
object DialPad : HomeTab(R.string.call_dial_pad_title)
}

View File

@ -73,7 +73,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ResendAll : RoomDetailAction() object ResendAll : RoomDetailAction()
data class StartCallWithPhoneNumber(val phoneNumber: String, val videoCall: Boolean): RoomDetailAction()
data class StartCall(val isVideo: Boolean) : RoomDetailAction() data class StartCall(val isVideo: Boolean) : RoomDetailAction()
data class AcceptCall(val callId: String): RoomDetailAction() data class AcceptCall(val callId: String): RoomDetailAction()
object EndCall : RoomDetailAction() object EndCall : RoomDetailAction()

View File

@ -320,7 +320,7 @@ class RoomDetailFragment @Inject constructor(
startCallActivityResultLauncher = startCallActivityResultLauncher, startCallActivityResultLauncher = startCallActivityResultLauncher,
showDialogWithMessage = ::showDialogWithMessage, showDialogWithMessage = ::showDialogWithMessage,
onTapToReturnToCall = ::onTapToReturnToCall onTapToReturnToCall = ::onTapToReturnToCall
).register() )
keyboardStateUtils = KeyboardStateUtils(requireActivity()) keyboardStateUtils = KeyboardStateUtils(requireActivity())
setupToolbar(views.roomToolbar) setupToolbar(views.roomToolbar)
setupRecyclerView() setupRecyclerView()

View File

@ -39,7 +39,6 @@ import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.conference.JitsiService
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.lookup.CallProtocolsChecker
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.command.CommandParser import im.vector.app.features.command.CommandParser
@ -176,7 +175,6 @@ class RoomDetailViewModel @AssistedInject constructor(
observeMyRoomMember() observeMyRoomMember()
observeActiveRoomWidgets() observeActiveRoomWidgets()
observePowerLevel() observePowerLevel()
updateShowDialerOptionState()
room.getRoomSummaryLive() room.getRoomSummaryLive()
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
@ -301,7 +299,6 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.EndCall -> handleEndCall()
@ -327,17 +324,6 @@ class RoomDetailViewModel @AssistedInject constructor(
}.exhaustive }.exhaustive
} }
private fun handleStartCallWithPhoneNumber(action: RoomDetailAction.StartCallWithPhoneNumber) {
viewModelScope.launch {
try {
val result = DialPadLookup(session, callManager, directRoomHelper).lookupPhoneNumber(action.phoneNumber)
callManager.startOutgoingCall(result.roomId, result.userId, action.videoCall)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
}
}
}
private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) { private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
callManager.getCallById(action.callId)?.also { callManager.getCallById(action.callId)?.also {
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
@ -1491,16 +1477,6 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
} }
override fun onPSTNSupportUpdated() {
updateShowDialerOptionState()
}
private fun updateShowDialerOptionState() {
setState {
copy(showDialerOption = callManager.supportsPSTNProtocol)
}
}
override fun onCleared() { override fun onCleared() {
roomSummariesHolder.remove(room.roomId) roomSummariesHolder.remove(room.roomId)
timeline.dispose() timeline.dispose()

View File

@ -75,7 +75,6 @@ data class RoomDetailViewState(
val canInvite: Boolean = true, val canInvite: Boolean = true,
val isAllowedToManageWidgets: Boolean = false, val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToStartWebRTCCall: Boolean = true,
val showDialerOption: Boolean = false,
val hasFailedSending: Boolean = false val hasFailedSending: Boolean = false
) : MvRxState { ) : MvRxState {

View File

@ -16,26 +16,18 @@
package im.vector.app.features.home.room.detail package im.vector.app.features.home.room.detail
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.Restorable
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.checkPermissions
import im.vector.app.features.call.DialerChoiceBottomSheet
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
import im.vector.app.features.call.dialpad.DialPadFragment
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
private const val DIALER_OPTION_TAG = "DIALER_OPTION_TAG"
private const val DIAL_PAD_TAG = "DIAL_PAD_TAG"
class StartCallActionsHandler( class StartCallActionsHandler(
private val roomId: String, private val roomId: String,
private val fragment: Fragment, private val fragment: Fragment,
@ -44,52 +36,20 @@ class StartCallActionsHandler(
private val roomDetailViewModel: RoomDetailViewModel, private val roomDetailViewModel: RoomDetailViewModel,
private val startCallActivityResultLauncher: ActivityResultLauncher<Array<String>>, private val startCallActivityResultLauncher: ActivityResultLauncher<Array<String>>,
private val showDialogWithMessage: (String) -> Unit, private val showDialogWithMessage: (String) -> Unit,
private val onTapToReturnToCall: () -> Unit): Restorable { private val onTapToReturnToCall: () -> Unit) {
fun onVideoCallClicked() { fun onVideoCallClicked() {
handleCallRequest(true) handleCallRequest(true)
} }
fun onVoiceCallClicked() = withState(roomDetailViewModel) { fun onVoiceCallClicked() {
if (it.showDialerOption) { handleCallRequest(false)
displayDialerChoiceBottomSheet()
} else {
handleCallRequest(false)
}
}
private fun DialerChoiceBottomSheet.applyListeners(): DialerChoiceBottomSheet {
onDialPadClicked = ::displayDialPadBottomSheet
onVoiceCallClicked = { handleCallRequest(false) }
return this
}
private fun CallDialPadBottomSheet.applyCallback(): CallDialPadBottomSheet {
callback = object : DialPadFragment.Callback {
override fun onOkClicked(formatted: String?, raw: String?) {
if (raw.isNullOrEmpty()) return
roomDetailViewModel.handle(RoomDetailAction.StartCallWithPhoneNumber(raw, false))
}
}
return this
}
private fun displayDialerChoiceBottomSheet() {
DialerChoiceBottomSheet()
.applyListeners()
.show(fragment.parentFragmentManager, DIALER_OPTION_TAG)
}
private fun displayDialPadBottomSheet() {
CallDialPadBottomSheet.newInstance(true)
.applyCallback()
.show(fragment.parentFragmentManager, DIAL_PAD_TAG)
} }
private fun handleCallRequest(isVideoCall: Boolean) = withState(roomDetailViewModel) { state -> private fun handleCallRequest(isVideoCall: Boolean) = withState(roomDetailViewModel) { state ->
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
when (roomSummary.joinedMembersCount) { when (roomSummary.joinedMembersCount) {
1 -> { 1 -> {
val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0 val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0
if (pendingInvite) { if (pendingInvite) {
// wait for other to join // wait for other to join
@ -99,7 +59,7 @@ class StartCallActionsHandler(
showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself)) showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself))
} }
} }
2 -> { 2 -> {
val currentCall = callManager.getCurrentCall() val currentCall = callManager.getCurrentCall()
if (currentCall != null) { if (currentCall != null) {
// resume existing if same room, if not prompt to kill and then restart new call? // resume existing if same room, if not prompt to kill and then restart new call?
@ -190,13 +150,4 @@ class StartCallActionsHandler(
} }
} }
} }
override fun onSaveInstanceState(outState: Bundle) = Unit
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
if (savedInstanceState != null) {
(fragment.parentFragmentManager.findFragmentByTag(DIALER_OPTION_TAG) as? DialerChoiceBottomSheet)?.applyListeners()
(fragment.parentFragmentManager.findFragmentByTag(DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.applyCallback()
}
}
} }

View File

@ -20,4 +20,11 @@
android:title="@string/bottom_action_notification" android:title="@string/bottom_action_notification"
android:visible="false" /> android:visible="false" />
<item
android:id="@+id/bottom_action_dial_pad"
android:enabled="true"
android:icon="@drawable/ic_call_dial_pad"
android:title="@string/call_dial_pad_title"
android:visible="false" />
</menu> </menu>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DialpadKeyNumberStyle">
<item name="android:textColor">?attr/vctr_content_primary</item>
<item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item>
<item name="android:gravity">center</item>
</style>
<style name="DialpadKeyLettersStyle">
<item name="android:textColor">?attr/vctr_content_secondary</item>
<item name="android:textSize">@dimen/dialpad_key_letters_size</item>
<item name="android:fontFamily">sans-serif-regular</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center_horizontal</item>
</style>
<style name="DialpadKeyPoundStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_pound_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
<style name="DialpadKeyStarStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_star_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
</resources>

View File

@ -1,12 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="DialpadKeyNumberStyle"> <style name="DialpadKeyNumberStyle">
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_primary</item>
<item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item> <item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item>
<item name="android:fontFamily">sans-serif-light</item> <item name="android:fontFamily">sans-serif</item>
<item name="android:layout_width">wrap_content</item> <item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item> <item name="android:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
</style> </style>
<style name="DialpadKeyLettersStyle">
<item name="android:textColor">?attr/vctr_content_secondary</item>
<item name="android:textSize">@dimen/dialpad_key_letters_size</item>
<item name="android:fontFamily">sans-serif-regular</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center_horizontal</item>
</style>
<style name="DialpadKeyPoundStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_pound_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
<style name="DialpadKeyStarStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_star_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
</resources> </resources>