diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt index 2cfc4b731f..ab85f979bf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt @@ -35,6 +35,11 @@ interface UserService { */ fun getUser(userId: String): User? + /** + * Try to resolve user from known users, or using profile api + */ + fun resolveUser(userId: String, callback: MatrixCallback) + /** * Search list of users on server directory. * @param search the searched term diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt index d2eb7a14ef..1740956915 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt @@ -19,10 +19,13 @@ package org.matrix.android.sdk.internal.session.user import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.user.accountdata.UpdateIgnoredUserIdsTask import org.matrix.android.sdk.internal.session.user.model.SearchUserTask import org.matrix.android.sdk.internal.task.TaskExecutor @@ -32,12 +35,40 @@ import javax.inject.Inject internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource, private val searchUserTask: SearchUserTask, private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask, + private val getProfileInfoTask: GetProfileInfoTask, private val taskExecutor: TaskExecutor) : UserService { override fun getUser(userId: String): User? { return userDataSource.getUser(userId) } + override fun resolveUser(userId: String, callback: MatrixCallback) { + val known = getUser(userId) + if (known != null) { + callback.onSuccess(known) + } else { + val params = GetProfileInfoTask.Params(userId) + getProfileInfoTask + .configureWith(params) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: JsonDict) { + callback.onSuccess( + User( + userId, + data[ProfileService.DISPLAY_NAME_KEY] as? String, + data[ProfileService.AVATAR_URL_KEY] as? String) + ) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + } + override fun getUserLive(userId: String): LiveData> { return userDataSource.getUserLive(userId) } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 7d2ca11813..e9bd03cb4b 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -81,7 +81,8 @@ android:resource="@xml/shortcuts" /> - + - + - diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 32a4af1b1b..7dde0edf32 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.view.MenuItem @@ -38,8 +39,12 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.utils.toast import im.vector.app.features.disclaimer.showDisclaimerDialog +import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.notifications.NotificationDrawerManager +import im.vector.app.features.permalink.NavigationInterceptor +import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert @@ -50,10 +55,12 @@ import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState import im.vector.app.push.fcm.FcmHelper +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.matrix.android.sdk.api.session.InitialSyncProgressService +import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.util.MatrixItem import timber.log.Timber import javax.inject.Inject @@ -64,7 +71,8 @@ data class HomeActivityArgs( val accountCreation: Boolean ) : Parcelable -class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory { +class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory, + NavigationInterceptor { private lateinit var sharedActionViewModel: HomeSharedActionViewModel @@ -82,6 +90,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var shortcutsHandler: ShortcutsHandler @Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory + @Inject lateinit var permalinkHandler: PermalinkHandler private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -145,6 +154,28 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet shortcutsHandler.observeRoomsAndBuildShortcuts() .disposeOnDestroy() + + if (isFirstCreation()) { + handleIntent(intent) + } + } + + private fun handleIntent(intent: Intent?) { + intent?.dataString?.let { deepLink -> + if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let + + permalinkHandler.launch(this, deepLink, + navigationInterceptor = this, + buildTask = true) + // .delay(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { isHandled -> + if (!isHandled) { + toast(R.string.permalink_malformed) + } + } + .disposeOnDestroy() + } } private fun renderState(state: HomeActivityViewState) { @@ -270,6 +301,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet if (intent?.getParcelableExtra(MvRx.KEY_ARG)?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + handleIntent(intent) } override fun onDestroy() { @@ -313,11 +345,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet bugReporter.openBugReportScreen(this, false) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } - R.id.menu_home_setting -> { + R.id.menu_home_setting -> { navigator.openSettings(this) return true } @@ -334,6 +366,18 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet } } + override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { + val listener = object : MatrixToBottomSheet.InteractionListener { + override fun navigateToRoom(roomId: String) { + navigator.openRoom(this@HomeActivity, roomId) + } + } + // TODO check if there is already one?? + MatrixToBottomSheet.withLink(deepLink.toString(), listener) + .show(supportFragmentManager, "HA#MatrixToBottomSheet") + return true + } + companion object { fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { val args = HomeActivityArgs( 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 e289234e7a..3f5e476a5e 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 @@ -1460,7 +1460,7 @@ class RoomDetailFragment @Inject constructor( return false } - override fun navToMemberProfile(userId: String): Boolean { + override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { openRoomMemberProfile(userId) return true } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt new file mode 100644 index 0000000000..e1c6800494 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToAction.kt @@ -0,0 +1,24 @@ +/* + * 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.matrixto + +import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.util.MatrixItem + +sealed class MatrixToAction : VectorViewModelAction { + data class StartChattingWithUser(val matrixItem: MatrixItem) : MatrixToAction() +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt index 91c09ef21a..69f30bb470 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt @@ -17,23 +17,37 @@ package im.vector.app.features.matrixto import android.os.Bundle +import android.os.Parcelable import android.view.View +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.features.home.AvatarRenderer +import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.* -import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject -class MatrixToBottomSheet(private val matrixItem: MatrixItem) : VectorBaseBottomSheetDialogFragment() { +class MatrixToBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Parcelize + data class MatrixToArgs( + val matrixToLink: String + ) : Parcelable @Inject lateinit var avatarRenderer: AvatarRenderer - interface InteractionListener { - fun didTapStartMessage(matrixItem: MatrixItem) - } + @Inject + lateinit var matrixToBottomSheetViewModelFactory: MatrixToBottomSheetViewModel.Factory override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -43,21 +57,87 @@ class MatrixToBottomSheet(private val matrixItem: MatrixItem) : VectorBaseBottom override fun getLayoutResId() = R.layout.bottom_sheet_matrix_to_card + private val viewModel by fragmentViewModel(MatrixToBottomSheetViewModel::class) + + interface InteractionListener { + fun navigateToRoom(roomId: String) + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + when (val item = state.matrixItem) { + Uninitialized -> { + matrixToCardContentLoading.isVisible = false + matrixToCardUserContentVisibility.isVisible = false + } + is Loading -> { + matrixToCardContentLoading.isVisible = true + matrixToCardUserContentVisibility.isVisible = false + } + is Success -> { + matrixToCardContentLoading.isVisible = false + matrixToCardUserContentVisibility.isVisible = true + matrixToCardNameText.setTextOrHide(item.invoke().displayName) + matrixToCardUserIdText.setTextOrHide(item.invoke().id) + avatarRenderer.render(item.invoke(), matrixToCardAvatar) + } + is Fail -> { + // TODO display some error copy? + dismiss() + } + } + + when (state.startChattingState) { + Uninitialized -> { + matrixToCardButtonLoading.isVisible = false + matrixToCardSendMessageButton.isVisible = false + } + is Success -> { + matrixToCardButtonLoading.isVisible = false + matrixToCardSendMessageButton.isVisible = true + } + is Fail -> { + matrixToCardButtonLoading.isVisible = false + matrixToCardSendMessageButton.isVisible = true + // TODO display some error copy? + dismiss() + } + is Loading -> { + matrixToCardButtonLoading.isVisible = true + matrixToCardSendMessageButton.isInvisible = true + } + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) matrixToCardSendMessageButton.debouncedClicks { - interactionListener?.didTapStartMessage(matrixItem) - dismiss() + withState(viewModel) { + it.matrixItem.invoke()?.let { item -> + viewModel.handle(MatrixToAction.StartChattingWithUser(item)) + } + } } - matrixToCardNameText.setTextOrHide(matrixItem.displayName) - matrixToCardUserIdText.setTextOrHide(matrixItem.id) - avatarRenderer.render(matrixItem, matrixToCardAvatar) + viewModel.observeViewEvents { + when (it) { + is MatrixToViewEvents.NavigateToRoom -> { + interactionListener?.navigateToRoom(it.roomId) + dismiss() + } + MatrixToViewEvents.Dismiss -> dismiss() + } + } } companion object { - fun create(matrixItem: MatrixItem, listener: InteractionListener?): MatrixToBottomSheet { - return MatrixToBottomSheet(matrixItem).apply { + fun withLink(matrixToLink: String, listener: InteractionListener?): MatrixToBottomSheet { + return MatrixToBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, MatrixToBottomSheet.MatrixToArgs( + matrixToLink = matrixToLink + )) + } interactionListener = listener } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt new file mode 100644 index 0000000000..9b1ce9fea8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt @@ -0,0 +1,33 @@ +/* + * 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.matrixto + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.util.MatrixItem + +data class MatrixToBottomSheetState( + val deepLink: String, + val matrixItem: Async = Uninitialized, + val startChattingState: Async = Uninitialized +) : MvRxState { + + constructor(args: MatrixToBottomSheet.MatrixToArgs) : this( + deepLink = args.matrixToLink + ) +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt new file mode 100644 index 0000000000..6e8a530c9a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -0,0 +1,166 @@ +/* + * 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.matrixto + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isE2EByDefault +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.permalinks.PermalinkData +import org.matrix.android.sdk.api.session.permalinks.PermalinkParser +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.internal.util.awaitCallback + +class MatrixToBottomSheetViewModel @AssistedInject constructor( + @Assisted initialState: MatrixToBottomSheetState, + private val session: Session, + private val stringProvider: StringProvider, + private val rawService: RawService) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: MatrixToBottomSheetState): MatrixToBottomSheetViewModel + } + + init { + setState { + copy(matrixItem = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + resolveLink(initialState) + } + } + + private suspend fun resolveLink(initialState: MatrixToBottomSheetState) { + val permalinkData = PermalinkParser.parse(initialState.deepLink) + if (permalinkData is PermalinkData.FallbackLink) { + setState { + copy( + matrixItem = Fail(IllegalArgumentException(stringProvider.getString(R.string.permalink_malformed))), + startChattingState = Uninitialized + ) + } + return + } + + when (permalinkData) { + is PermalinkData.UserLink -> { + val user = resolveUser(permalinkData.userId) + setState { + copy( + matrixItem = Success(user.toMatrixItem()), + startChattingState = Success(Unit) + ) + } + } + is PermalinkData.RoomLink -> { + // not yet supported + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + is PermalinkData.GroupLink -> { + // not yet supported + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + is PermalinkData.FallbackLink -> { + _viewEvents.post(MatrixToViewEvents.Dismiss) + } + } + } + + private suspend fun resolveUser(userId: String): User { + return tryOrNull { + awaitCallback { + session.resolveUser(userId, it) + } + } + // Create raw user in case the user is not searchable + ?: User(userId, null, null) + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: MatrixToBottomSheetState): MatrixToBottomSheetViewModel? { + val fragment: MatrixToBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + + return fragment.matrixToBottomSheetViewModelFactory.create(state) + } + } + + override fun handle(action: MatrixToAction) { + when (action) { + is MatrixToAction.StartChattingWithUser -> handleStartChatting(action) + }.exhaustive + } + + private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) { + val mxId = action.matrixItem.id + val existing = session.getExistingDirectRoomWithUser(mxId) + if (existing != null) { + // navigate to this room + _viewEvents.post(MatrixToViewEvents.NavigateToRoom(existing)) + } else { + setState { + copy(startChattingState = Loading()) + } + // we should create the room then navigate + viewModelScope.launch(Dispatchers.IO) { + val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) + ?.isE2EByDefault() + ?: true + + val roomParams = CreateRoomParams() + .apply { + invitedUserIds.add(mxId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault + } + + val roomId = try { + awaitCallback { session.createRoom(roomParams, it) } + } catch (failure: Throwable) { + setState { + copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure)))) + } + return@launch + } + setState { + // we can hide this button has we will navigate out + copy(startChattingState = Uninitialized) + } + _viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt new file mode 100644 index 0000000000..f9491fd361 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToViewEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.matrixto + +import im.vector.app.core.platform.VectorViewEvents + +sealed class MatrixToViewEvents : VectorViewEvents { + data class NavigateToRoom(val roomId: String) : MatrixToViewEvents() + object Dismiss : MatrixToViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index 11c55f6a73..f1149d8990 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -63,13 +63,14 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .flatMap { permalinkData -> - handlePermalink(permalinkData, context, navigationInterceptor, buildTask) + handlePermalink(permalinkData, deepLink, context, navigationInterceptor, buildTask) } .onErrorReturnItem(false) } private fun handlePermalink( permalinkData: PermalinkData, + rawLink: Uri, context: Context, navigationInterceptor: NavigationInterceptor?, buildTask: Boolean @@ -96,7 +97,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti Single.just(true) } is PermalinkData.UserLink -> { - if (navigationInterceptor?.navToMemberProfile(permalinkData.userId) != true) { + if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) { navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask) } Single.just(true) @@ -175,7 +176,7 @@ interface NavigationInterceptor { /** * Return true if the navigation has been intercepted */ - fun navToMemberProfile(userId: String): Boolean { + fun navToMemberProfile(userId: String, deepLink: Uri): Boolean { return false } } diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt index e005dd06c5..e8064aaec5 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandlerActivity.kt @@ -23,11 +23,9 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.utils.toast +import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.LoadingFragment import im.vector.app.features.login.LoginActivity -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit import javax.inject.Inject class PermalinkHandlerActivity : VectorBaseActivity() { @@ -45,23 +43,28 @@ class PermalinkHandlerActivity : VectorBaseActivity() { if (isFirstCreation()) { replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java) } + handleIntent() + } + + private fun handleIntent() { // If we are not logged in, open login screen. // In the future, we might want to relaunch the process after login. if (!sessionHolder.hasActiveSession()) { startLoginActivity() return } - val uri = intent.dataString - permalinkHandler.launch(this, uri, buildTask = true) - .delay(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { isHandled -> - if (!isHandled) { - toast(R.string.permalink_malformed) - } - finish() - } - .disposeOnDestroy() + // We forward intent to HomeActivity (singleTask) to avoid the dueling app problem + // https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances + intent.setClass(this, HomeActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + startActivity(intent) + + finish() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent() } private fun startLoginActivity() { diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt index ffef98d544..547e2d939f 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -36,7 +36,6 @@ import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.features.matrixto.MatrixToBottomSheet import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_simple.* -import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject import kotlin.reflect.KClass @@ -72,7 +71,7 @@ class UserCodeActivity UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY) is UserCodeState.Mode.RESULT -> { showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) - MatrixToBottomSheet.create(mode.matrixItem, this).show(supportFragmentManager, "MatrixToBottomSheet") + MatrixToBottomSheet.withLink(mode.rawLink, this).show(supportFragmentManager, "MatrixToBottomSheet") } } } @@ -104,8 +103,8 @@ class UserCodeActivity } } - override fun didTapStartMessage(matrixItem: MatrixItem) { - sharedViewModel.handle(UserCodeActions.StartChattingWithUser(matrixItem)) + override fun navigateToRoom(roomId: String) { + navigator.openRoom(this, roomId) } override fun onBackPressed() = withState(sharedViewModel) { diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index abef15a1ba..45b6f0ee65 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -30,6 +30,7 @@ import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.permalinks.PermalinkData @@ -72,12 +73,12 @@ class UserCodeSharedViewModel @AssistedInject constructor( override fun handle(action: UserCodeActions) { when (action) { - UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss) - is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } - is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) - is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) + UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss) + is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } + is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) + is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted) - UserCodeActions.ShareByText -> handleShareByText() + UserCodeActions.ShareByText -> handleShareByText() } } @@ -139,22 +140,33 @@ class UserCodeSharedViewModel @AssistedInject constructor( _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen) viewModelScope.launch(Dispatchers.IO) { when (linkedId) { - is PermalinkData.RoomLink -> TODO() - is PermalinkData.UserLink -> { - val user = session.getUser(linkedId.userId) ?: awaitCallback> { - session.searchUsersDirectory(linkedId.userId, 10, emptySet(), it) - }.firstOrNull { it.userId == linkedId.userId } + is PermalinkData.RoomLink -> { + // not yet supported + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) + } + is PermalinkData.UserLink -> { + val user = tryOrNull { + awaitCallback { + session.resolveUser(linkedId.userId, it) + } + } // Create raw Uxid in case the user is not searchable - ?: User(linkedId.userId, null, null) + ?: User(linkedId.userId, null, null) setState { copy( - mode = UserCodeState.Mode.RESULT(user.toMatrixItem()) + mode = UserCodeState.Mode.RESULT(user.toMatrixItem(), action.code) ) } } - is PermalinkData.GroupLink -> TODO() - is PermalinkData.FallbackLink -> TODO() + is PermalinkData.GroupLink -> { + // not yet supported + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) + } + is PermalinkData.FallbackLink -> { + // not yet supported + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) + } } _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen) } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt index fdcfa2cb5e..c26da7c0a4 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt @@ -28,7 +28,7 @@ data class UserCodeState( sealed class Mode { object SHOW : Mode() object SCAN : Mode() - data class RESULT(val matrixItem: MatrixItem) : Mode() + data class RESULT(val matrixItem: MatrixItem, val rawLink: String) : Mode() } constructor(args: UserCodeActivity.Args) : this( diff --git a/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml index b8c81ded3a..d051bd7c98 100644 --- a/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml +++ b/vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml @@ -3,13 +3,24 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:minHeight="200dp"> + + + + + +