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..e3e942fce9 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 @@ -38,8 +38,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 +54,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 +70,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 +89,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) { @@ -117,9 +125,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet .observe() .subscribe { sharedAction -> when (sharedAction) { - is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { + is HomeActivitySharedAction.OpenGroup -> { drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) } @@ -136,20 +144,42 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet homeActivityViewModel.observeViewEvents { when (it) { is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) - is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) - HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() - is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) + is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) + HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() + is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) }.exhaustive } homeActivityViewModel.subscribe(this) { renderState(it) } 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) { when (val status = state.initialSyncProgressServiceStatus) { - is InitialSyncProgressService.Status.Idle -> { + is InitialSyncProgressService.Status.Idle -> { waiting_view.isVisible = false } is InitialSyncProgressService.Status.Progressing -> { @@ -270,6 +300,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet if (intent?.getParcelableExtra(MvRx.KEY_ARG)?.clearNotification == true) { notificationDrawerManager.clearAllEvents() } + handleIntent(intent) } override fun onDestroy() { @@ -313,11 +344,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 +365,18 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet } } + override fun navToMemberProfile(userId: String): Boolean { + val listener = object : MatrixToBottomSheet.InteractionListener { + override fun navigateToRoom(roomId: String) { + navigator.openRoom(this@HomeActivity, roomId) + } + } + // TODO check if there is already one?? + MatrixToBottomSheet.withUserId(userId, 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/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..41020ea404 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,38 @@ 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?, + val userId: 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 +58,100 @@ 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, + userId = null + )) + } + interactionListener = listener + } + } + + fun withUserId(userId: String, listener: InteractionListener?): MatrixToBottomSheet { + return MatrixToBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, MatrixToBottomSheet.MatrixToArgs( + matrixToLink = null, + userId = userId + )) + } 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..0080b28c66 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt @@ -0,0 +1,27 @@ +/* + * 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 matrixItem: Async = Uninitialized, + val startChattingState: Async = Uninitialized +) : MvRxState 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..06cae3218b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -0,0 +1,192 @@ +/* + * 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.profile.ProfileService +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.JsonDict +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.internal.util.awaitCallback + +class MatrixToBottomSheetViewModel @AssistedInject constructor( + @Assisted initialState: MatrixToBottomSheetState, + @Assisted val args: MatrixToBottomSheet.MatrixToArgs, + private val session: Session, + private val stringProvider: StringProvider, + private val rawService: RawService) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: MatrixToBottomSheetState, + args: MatrixToBottomSheet.MatrixToArgs): MatrixToBottomSheetViewModel + } + + init { + setState { + copy(matrixItem = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + resolveLink() + } + } + + private suspend fun resolveLink() { + when { + args.matrixToLink != null -> { + val linkedId = PermalinkParser.parse(args.matrixToLink) + if (linkedId is PermalinkData.FallbackLink) { + setState { + copy( + matrixItem = Fail(IllegalArgumentException(stringProvider.getString(R.string.permalink_malformed))), + startChattingState = Uninitialized + ) + } + return + } + + when (linkedId) { + is PermalinkData.UserLink -> { + val user = resolveUser(linkedId.userId) + setState { + copy( + matrixItem = Success(user.toMatrixItem()), + startChattingState = Success(Unit) + ) + } + } + is PermalinkData.RoomLink -> TODO() + is PermalinkData.GroupLink -> { + // not yet supported + } + is PermalinkData.FallbackLink -> { + } + } + } + args.userId != null -> { + val user = resolveUser(args.userId) + + setState { + copy( + matrixItem = Success(user.toMatrixItem()), + startChattingState = Success(Unit) + ) + } + } + else -> { + setState { + copy( + matrixItem = Fail(IllegalArgumentException(stringProvider.getString(R.string.unexpected_error))), + startChattingState = Uninitialized + ) + } + } + } + } + + private suspend fun resolveUser(userId: String): User { + return (session.getUser(userId) + ?: tryOrNull { + awaitCallback { + session.getProfile(userId, it) + } + }?.let { + User(userId, it[ProfileService.DISPLAY_NAME_KEY] as? String, it[ProfileService.AVATAR_URL_KEY] as? String) + } + // Create raw Uxid 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() + val args: MatrixToBottomSheet.MatrixToArgs = viewModelContext.args() + + return fragment.matrixToBottomSheetViewModelFactory.create(state, args) + } + } + + 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) }.also { + setState { + copy(startChattingState = Success(Unit)) + } + } + } catch (failure: Throwable) { + setState { + copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure)))) + } + return@launch + } + _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/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..d6279470ae 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.withUserId(mode.matrixItem.id, 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..93b198f9d7 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,12 +30,15 @@ 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.profile.ProfileService 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.JsonDict import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.util.awaitCallback @@ -72,12 +75,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,13 +142,21 @@ 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 } - // Create raw Uxid in case the user is not searchable - ?: User(linkedId.userId, null, null) + is PermalinkData.RoomLink -> { + // not yet supported + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented))) + } + is PermalinkData.UserLink -> { + val user = session.getUser(linkedId.userId) + ?: tryOrNull { + awaitCallback { + session.getProfile(linkedId.userId, it) + } + }?.let { + User(linkedId.userId, it[ProfileService.DISPLAY_NAME_KEY] as? String, it[ProfileService.AVATAR_URL_KEY] as? String) + } + // Create raw Uxid in case the user is not searchable + ?: User(linkedId.userId, null, null) setState { copy( @@ -153,8 +164,14 @@ class UserCodeSharedViewModel @AssistedInject constructor( ) } } - 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/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"> + + + + + +