diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt index 441232f57f..c746ad863a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt @@ -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()) { DaggerSessionComponent .factory() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt index e297f59b96..a9e5089774 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt @@ -31,9 +31,11 @@ internal class DirectChatsHelper @Inject constructor(@SessionDatabase */ fun getLocalUserAccount(filterRoomId: String? = null): MutableMap> { 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) .asSequence() - .filter { it.roomId != filterRoomId && it.directUserId != null } + .filter { it.roomId != filterRoomId && it.directUserId != null && it.membership.isActive() } .groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId }) } } diff --git a/newsfragment/3333.bugfix b/newsfragment/3333.bugfix new file mode 100644 index 0000000000..d60f725498 --- /dev/null +++ b/newsfragment/3333.bugfix @@ -0,0 +1 @@ +Fix new DMs not always marked as such \ No newline at end of file diff --git a/newsfragment/3457.misc b/newsfragment/3457.misc new file mode 100644 index 0000000000..6a70641cfd --- /dev/null +++ b/newsfragment/3457.misc @@ -0,0 +1 @@ +Move the ability to start a call from dialpad directly to a dedicated tab in the home screen. \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index b5f45e6586..c0219b26e2 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -116,7 +116,9 @@ class DefaultErrorFormatter @Inject constructor( 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) else -> throwable.localizedMessage } diff --git a/vector/src/main/java/im/vector/app/features/call/DialerChoiceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/DialerChoiceBottomSheet.kt deleted file mode 100644 index 401b3e23d7..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/DialerChoiceBottomSheet.kt +++ /dev/null @@ -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() { - 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() - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt index fe0afc166d..e388cd56fa 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt @@ -26,9 +26,9 @@ import androidx.core.widget.ImageViewCompat import androidx.fragment.app.Fragment import com.android.dialer.dialpadview.DialpadView import com.android.dialer.dialpadview.DigitsEditText -import com.android.dialer.dialpadview.R import com.google.i18n.phonenumbers.AsYouTypeFormatter import com.google.i18n.phonenumbers.PhoneNumberUtil +import im.vector.app.R import im.vector.app.features.themes.ThemeUtils class DialPadFragment : Fragment() { @@ -57,7 +57,7 @@ class DialPadFragment : Fragment() { dialpadView.findViewById(R.id.dialpad_key_voicemail).isVisible = false digits = dialpadView.digits as? DigitsEditText 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(R.id.zero).setOnClickListener { append('0') } if (enablePlus) { dialpadView.findViewById(R.id.zero).setOnLongClickListener { diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index 4ed1e4a0db..4f025dc239 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -17,10 +17,11 @@ package im.vector.app.features.call.dialpad 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.createdirect.DirectRoomHelper import org.matrix.android.sdk.api.session.Session -import java.lang.IllegalStateException import javax.inject.Inject class DialPadLookup @Inject constructor( @@ -28,13 +29,25 @@ class DialPadLookup @Inject constructor( private val webRtcCallManager: WebRtcCallManager, 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) suspend fun lookupPhoneNumber(phoneNumber: String): Result { - val thirdPartyUser = session.pstnLookup(phoneNumber, webRtcCallManager.supportedPSTNProtocol).firstOrNull() ?: throw IllegalStateException() - val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId) - return Result(userId = thirdPartyUser.userId, roomId = roomId) + session.vectorCallService.protocolChecker.awaitCheckProtocols() + val thirdPartyUser = session.pstnLookup(phoneNumber, webRtcCallManager.supportedPSTNProtocol).firstOrNull() ?: throw Failure.NoResult + // 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) } } diff --git a/vector/src/main/java/im/vector/app/features/call/lookup/ThirdPartyLookup.kt b/vector/src/main/java/im/vector/app/features/call/lookup/ThirdPartyLookup.kt index 1e9834059f..dfdc58f78f 100644 --- a/vector/src/main/java/im/vector/app/features/call/lookup/ThirdPartyLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/lookup/ThirdPartyLookup.kt @@ -16,10 +16,13 @@ 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.session.Session 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 { if (protocol == null) return emptyList() return tryOrNull { @@ -36,7 +39,11 @@ suspend fun Session.sipVirtualLookup(nativeMxid: String): List { protocol = PROTOCOL_SIP_VIRTUAL, fields = mapOf("native_mxid" to nativeMxid) ) - }.orEmpty() + } + .orEmpty() + .filter { + (it.fields[LOOKUP_SUCCESS_FIELD] as? Boolean).orFalse() + } } suspend fun Session.sipNativeLookup(virtualMxid: String): List { @@ -45,5 +52,9 @@ suspend fun Session.sipNativeLookup(virtualMxid: String): List { protocol = PROTOCOL_SIP_NATIVE, fields = mapOf("virtual_mxid" to virtualMxid) ) - }.orEmpty() + } + .orEmpty() + .filter { + (it.fields[LOOKUP_SUCCESS_FIELD] as? Boolean).orFalse() + } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt index c64f9d453d..b466f204ec 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home import im.vector.app.core.platform.VectorViewModelAction sealed class HomeDetailAction : VectorViewModelAction { - data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction() + data class SwitchTab(val tab: HomeTab) : HomeDetailAction() object MarkAllRoomsRead : HomeDetailAction() + data class StartCallWithPhoneNumber(val phoneNumber: String): HomeDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 488e42c7ef..49efb7f5c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -23,6 +23,8 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.core.view.iterator +import androidx.fragment.app.Fragment import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel 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.features.call.SharedKnownCallsViewModel 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.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.popup.PopupAlertManager 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.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.app.features.themes.ThemeUtils @@ -101,6 +105,9 @@ class HomeDetailFragment @Inject constructor( } 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 super.onPrepareOptionsMenu(menu) } @@ -123,7 +130,7 @@ class HomeDetailFragment @Inject constructor( withState(viewModel) { // 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 -> @@ -137,8 +144,20 @@ class HomeDetailFragment @Inject constructor( } } - viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> - switchDisplayMode(displayMode) + viewModel.selectSubscribe(this, HomeDetailViewState::currentTab) { currentTab -> + 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 -> @@ -179,20 +198,8 @@ class HomeDetailFragment @Inject constructor( override fun onResume() { super.onResume() // update notification tab if needed - checkNotificationTabStatus() - } - - 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)) - } - } - } + updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab()) + callManager.checkForProtocolsSupportIfNeeded() } private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) { @@ -321,12 +328,13 @@ class HomeDetailFragment @Inject constructor( private fun setupBottomNavigationView() { views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab() views.bottomNavigationView.setOnNavigationItemSelectedListener { - val displayMode = when (it.itemId) { - R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE - R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS - else -> RoomListDisplayMode.NOTIFICATIONS + val tab = when (it.itemId) { + R.id.bottom_action_people -> HomeTab.RoomList(RoomListDisplayMode.PEOPLE) + R.id.bottom_action_rooms -> HomeTab.RoomList(RoomListDisplayMode.ROOMS) + R.id.bottom_action_notification -> HomeTab.RoomList(RoomListDisplayMode.NOTIFICATIONS) + else -> HomeTab.DialPad } - viewModel.handle(HomeDetailAction.SwitchDisplayMode(displayMode)) + viewModel.handle(HomeDetailAction.SwitchTab(tab)) true } @@ -342,13 +350,14 @@ class HomeDetailFragment @Inject constructor( // } } - private fun switchDisplayMode(displayMode: RoomListDisplayMode) { - views.groupToolbarTitleView.setText(displayMode.titleRes) - updateSelectedFragment(displayMode) + private fun updateUIForTab(tab: HomeTab) { + views.groupToolbarTitleView.setText(tab.titleRes) + updateSelectedFragment(tab) + invalidateOptionsMenu() } - private fun updateSelectedFragment(displayMode: RoomListDisplayMode) { - val fragmentTag = "FRAGMENT_TAG_${displayMode.name}" + private fun updateSelectedFragment(tab: HomeTab) { + val fragmentTag = "FRAGMENT_TAG_$tab" val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag) childFragmentManager.commitTransaction { childFragmentManager.fragments @@ -357,14 +366,49 @@ class HomeDetailFragment @Inject constructor( detach(it) } if (fragmentToShow == null) { - val params = RoomListParams(displayMode) - add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag) + when (tab) { + 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 { + if (tab is HomeTab.DialPad) { + (fragmentToShow as? DialPadFragment)?.applyCallback() + } 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 * ========================================================================================== */ @@ -399,10 +443,13 @@ class HomeDetailFragment @Inject constructor( } } - private fun RoomListDisplayMode.toMenuId() = when (this) { - RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people - RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms - else -> R.id.bottom_action_notification + private fun HomeTab.toMenuId() = when (this) { + is HomeTab.DialPad -> R.id.bottom_action_dial_pad + is HomeTab.RoomList -> when (displayMode) { + RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people + RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms + else -> R.id.bottom_action_notification + } } 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 { return serverBackupStatusViewModelFactory.create(initialState) } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewEvents.kt new file mode 100644 index 0000000000..a0ff67dc0a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index 836c63e85b..b6210ae019 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -26,8 +26,11 @@ import dagger.assisted.AssistedInject import im.vector.app.AppStateHandler import im.vector.app.RoomGroupingMethod 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.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 io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers @@ -51,8 +54,11 @@ import java.util.concurrent.TimeUnit class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, private val uiStateRepository: UiStateRepository, + private val callManager: WebRtcCallManager, + private val directRoomHelper: DirectRoomHelper, private val appStateHandler: AppStateHandler) - : VectorViewModel(initialState) { + : VectorViewModel(initialState), + CallProtocolsChecker.Listener { @AssistedFactory interface Factory { @@ -64,7 +70,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? { val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository() return HomeDetailViewState( - displayMode = uiStateRepository.getDisplayMode() + currentTab = HomeTab.RoomList(uiStateRepository.getDisplayMode()) ) } @@ -79,7 +85,8 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho observeSyncState() observeRoomGroupingMethod() observeRoomSummaries() - + updateShowDialPadTab() + callManager.addProtocolsCheckerListener(this) session.rx().liveUser(session.myUserId).execute { copy( myMatrixItem = it.invoke()?.getOrNull()?.toMatrixItem() @@ -89,18 +96,48 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho override fun handle(action: HomeDetailAction) { when (action) { - is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action) - HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() + is HomeDetailAction.SwitchTab -> handleSwitchTab(action) + HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() + is HomeDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) } } - private fun handleSwitchDisplayMode(action: HomeDetailAction.SwitchDisplayMode) = withState { state -> - if (state.displayMode != action.displayMode) { - setState { - copy(displayMode = action.displayMode) + private fun handleStartCallWithPhoneNumber(action: HomeDetailAction.StartCallWithPhoneNumber) { + viewModelScope.launch { + try { + _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() { appStateHandler.selectedRoomGroupingObservable .subscribe { - setState { - copy( - roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null) - ) - } + setState { + copy( + roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null) + ) + } } .disposeOnClear() } @@ -165,7 +202,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho is RoomGroupingMethod.ByLegacyGroup -> { // TODO!! } - is RoomGroupingMethod.BySpace -> { + is RoomGroupingMethod.BySpace -> { val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId val dmInvites = session.getRoomSummaries( roomSummaryQueryParams { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 5aa9612a7a..304444abdd 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -16,9 +16,11 @@ package im.vector.app.features.home +import androidx.annotation.StringRes import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.app.R import im.vector.app.RoomGroupingMethod import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.sync.SyncState @@ -28,7 +30,7 @@ data class HomeDetailViewState( val roomGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null), val myMatrixItem: MatrixItem? = null, val asyncRooms: Async> = Uninitialized, - val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE, + val currentTab: HomeTab = HomeTab.RoomList(RoomListDisplayMode.PEOPLE), val notificationCountCatchup: Int = 0, val notificationHighlightCatchup: Boolean = false, val notificationCountPeople: Int = 0, @@ -36,5 +38,11 @@ data class HomeDetailViewState( val notificationCountRooms: Int = 0, val notificationHighlightRooms: Boolean = false, val hasUnreadMessages: Boolean = false, - val syncState: SyncState = SyncState.Idle + val syncState: SyncState = SyncState.Idle, + val showDialPadTab: Boolean = false ) : 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) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 72e614c18c..c0e73823e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -73,7 +73,6 @@ sealed class RoomDetailAction : VectorViewModelAction { object ResendAll : RoomDetailAction() - data class StartCallWithPhoneNumber(val phoneNumber: String, val videoCall: Boolean): RoomDetailAction() data class StartCall(val isVideo: Boolean) : RoomDetailAction() data class AcceptCall(val callId: String): RoomDetailAction() object EndCall : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 0d4fcc31b5..8307e93576 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -320,7 +320,7 @@ class RoomDetailFragment @Inject constructor( startCallActivityResultLauncher = startCallActivityResultLauncher, showDialogWithMessage = ::showDialogWithMessage, onTapToReturnToCall = ::onTapToReturnToCall - ).register() + ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(views.roomToolbar) setupRecyclerView() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index a2041c0a80..751114c2d9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -39,7 +39,6 @@ import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider 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.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser @@ -176,7 +175,6 @@ class RoomDetailViewModel @AssistedInject constructor( observeMyRoomMember() observeActiveRoomWidgets() observePowerLevel() - updateShowDialerOptionState() room.getRoomSummaryLive() viewModelScope.launch(Dispatchers.IO) { tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } @@ -301,7 +299,6 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() - is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action) is RoomDetailAction.EndCall -> handleEndCall() @@ -327,17 +324,6 @@ class RoomDetailViewModel @AssistedInject constructor( }.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) { callManager.getCallById(action.callId)?.also { _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) @@ -1491,16 +1477,6 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) } - override fun onPSTNSupportUpdated() { - updateShowDialerOptionState() - } - - private fun updateShowDialerOptionState() { - setState { - copy(showDialerOption = callManager.supportsPSTNProtocol) - } - } - override fun onCleared() { roomSummariesHolder.remove(room.roomId) timeline.dispose() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 965733c424..1ead34fadd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -75,7 +75,6 @@ data class RoomDetailViewState( val canInvite: Boolean = true, val isAllowedToManageWidgets: Boolean = false, val isAllowedToStartWebRTCCall: Boolean = true, - val showDialerOption: Boolean = false, val hasFailedSending: Boolean = false ) : MvRxState { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt index 6f9a4fa651..b71b90ace3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -16,26 +16,18 @@ package im.vector.app.features.home.room.detail -import android.os.Bundle import androidx.activity.result.ActivityResultLauncher import androidx.fragment.app.Fragment import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder 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_VIDEO_IP_CALL 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.settings.VectorPreferences 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( private val roomId: String, private val fragment: Fragment, @@ -44,52 +36,20 @@ class StartCallActionsHandler( private val roomDetailViewModel: RoomDetailViewModel, private val startCallActivityResultLauncher: ActivityResultLauncher>, private val showDialogWithMessage: (String) -> Unit, - private val onTapToReturnToCall: () -> Unit): Restorable { + private val onTapToReturnToCall: () -> Unit) { fun onVideoCallClicked() { handleCallRequest(true) } - fun onVoiceCallClicked() = withState(roomDetailViewModel) { - if (it.showDialerOption) { - 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) + fun onVoiceCallClicked() { + handleCallRequest(false) } private fun handleCallRequest(isVideoCall: Boolean) = withState(roomDetailViewModel) { state -> val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState when (roomSummary.joinedMembersCount) { - 1 -> { + 1 -> { val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0 if (pendingInvite) { // wait for other to join @@ -99,7 +59,7 @@ class StartCallActionsHandler( showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself)) } } - 2 -> { + 2 -> { val currentCall = callManager.getCurrentCall() if (currentCall != null) { // 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() - } - } } diff --git a/vector/src/main/res/menu/home_bottom_navigation.xml b/vector/src/main/res/menu/home_bottom_navigation.xml index d52c8afab3..f9f966003a 100644 --- a/vector/src/main/res/menu/home_bottom_navigation.xml +++ b/vector/src/main/res/menu/home_bottom_navigation.xml @@ -20,4 +20,11 @@ android:title="@string/bottom_action_notification" android:visible="false" /> + + diff --git a/vector/src/main/res/values-land/styles_dial_pad.xml b/vector/src/main/res/values-land/styles_dial_pad.xml new file mode 100644 index 0000000000..39c5bf9aa6 --- /dev/null +++ b/vector/src/main/res/values-land/styles_dial_pad.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/styles_dial_pad.xml b/vector/src/main/res/values/styles_dial_pad.xml index 9a2737c0d4..34e128c56d 100644 --- a/vector/src/main/res/values/styles_dial_pad.xml +++ b/vector/src/main/res/values/styles_dial_pad.xml @@ -1,12 +1,33 @@ + + + + + + + + \ No newline at end of file