Merge pull request #2444 from vector-im/feature/bca/deeplink_mxto

Fix issues with matrix.to deep linking
This commit is contained in:
Benoit Marty 2020-11-27 10:22:51 +01:00 committed by GitHub
commit bc889cbcf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 509 additions and 57 deletions

View File

@ -35,6 +35,11 @@ interface UserService {
*/ */
fun getUser(userId: String): User? fun getUser(userId: String): User?
/**
* Try to resolve user from known users, or using profile api
*/
fun resolveUser(userId: String, callback: MatrixCallback<User>)
/** /**
* Search list of users on server directory. * Search list of users on server directory.
* @param search the searched term * @param search the searched term

View File

@ -19,10 +19,13 @@ package org.matrix.android.sdk.internal.session.user
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback 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.UserService
import org.matrix.android.sdk.api.session.user.model.User 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.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional 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.accountdata.UpdateIgnoredUserIdsTask
import org.matrix.android.sdk.internal.session.user.model.SearchUserTask import org.matrix.android.sdk.internal.session.user.model.SearchUserTask
import org.matrix.android.sdk.internal.task.TaskExecutor 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, internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource,
private val searchUserTask: SearchUserTask, private val searchUserTask: SearchUserTask,
private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask, private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask,
private val getProfileInfoTask: GetProfileInfoTask,
private val taskExecutor: TaskExecutor) : UserService { private val taskExecutor: TaskExecutor) : UserService {
override fun getUser(userId: String): User? { override fun getUser(userId: String): User? {
return userDataSource.getUser(userId) return userDataSource.getUser(userId)
} }
override fun resolveUser(userId: String, callback: MatrixCallback<User>) {
val known = getUser(userId)
if (known != null) {
callback.onSuccess(known)
} else {
val params = GetProfileInfoTask.Params(userId)
getProfileInfoTask
.configureWith(params) {
this.callback = object : MatrixCallback<JsonDict> {
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<Optional<User>> { override fun getUserLive(userId: String): LiveData<Optional<User>> {
return userDataSource.getUserLive(userId) return userDataSource.getUserLive(userId)
} }

View File

@ -81,7 +81,8 @@
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity-alias> </activity-alias>
<activity android:name=".features.home.HomeActivity" /> <activity android:name=".features.home.HomeActivity"
android:launchMode="singleTask"/>
<activity <activity
android:name=".features.login.LoginActivity" android:name=".features.login.LoginActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
@ -189,10 +190,9 @@
<activity <activity
android:name=".features.signout.soft.SoftLogoutActivity" android:name=".features.signout.soft.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity android:name=".features.permalink.PermalinkHandlerActivity"> <activity android:name=".features.permalink.PermalinkHandlerActivity" android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.MenuItem 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.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.pushers.PushersManager 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.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.notifications.NotificationDrawerManager 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.DefaultVectorAlert
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.popup.VerificationVectorAlert
@ -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.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.matrix.android.sdk.api.session.InitialSyncProgressService 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 org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -64,7 +71,8 @@ data class HomeActivityArgs(
val accountCreation: Boolean val accountCreation: Boolean
) : Parcelable ) : Parcelable
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory { class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory,
NavigationInterceptor {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
@ -82,6 +90,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var shortcutsHandler: ShortcutsHandler @Inject lateinit var shortcutsHandler: ShortcutsHandler
@Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory @Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory
@Inject lateinit var permalinkHandler: PermalinkHandler
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
@ -145,6 +154,28 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
shortcutsHandler.observeRoomsAndBuildShortcuts() shortcutsHandler.observeRoomsAndBuildShortcuts()
.disposeOnDestroy() .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) { private fun renderState(state: HomeActivityViewState) {
@ -270,6 +301,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
if (intent?.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)?.clearNotification == true) { if (intent?.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)?.clearNotification == true) {
notificationDrawerManager.clearAllEvents() notificationDrawerManager.clearAllEvents()
} }
handleIntent(intent)
} }
override fun onDestroy() { override fun onDestroy() {
@ -313,11 +345,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
bugReporter.openBugReportScreen(this, false) bugReporter.openBugReportScreen(this, false)
return true return true
} }
R.id.menu_home_filter -> { R.id.menu_home_filter -> {
navigator.openRoomsFiltering(this) navigator.openRoomsFiltering(this)
return true return true
} }
R.id.menu_home_setting -> { R.id.menu_home_setting -> {
navigator.openSettings(this) navigator.openSettings(this)
return true 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 { companion object {
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
val args = HomeActivityArgs( val args = HomeActivityArgs(

View File

@ -1460,7 +1460,7 @@ class RoomDetailFragment @Inject constructor(
return false return false
} }
override fun navToMemberProfile(userId: String): Boolean { override fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
openRoomMemberProfile(userId) openRoomMemberProfile(userId)
return true return true
} }

View File

@ -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()
}

View File

@ -17,23 +17,37 @@
package im.vector.app.features.matrixto package im.vector.app.features.matrixto
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.View 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.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.* import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject 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 @Inject lateinit var avatarRenderer: AvatarRenderer
interface InteractionListener { @Inject
fun didTapStartMessage(matrixItem: MatrixItem) lateinit var matrixToBottomSheetViewModelFactory: MatrixToBottomSheetViewModel.Factory
}
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
@ -43,21 +57,87 @@ class MatrixToBottomSheet(private val matrixItem: MatrixItem) : VectorBaseBottom
override fun getLayoutResId() = R.layout.bottom_sheet_matrix_to_card 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
matrixToCardSendMessageButton.debouncedClicks { matrixToCardSendMessageButton.debouncedClicks {
interactionListener?.didTapStartMessage(matrixItem) withState(viewModel) {
dismiss() it.matrixItem.invoke()?.let { item ->
viewModel.handle(MatrixToAction.StartChattingWithUser(item))
}
}
} }
matrixToCardNameText.setTextOrHide(matrixItem.displayName) viewModel.observeViewEvents {
matrixToCardUserIdText.setTextOrHide(matrixItem.id) when (it) {
avatarRenderer.render(matrixItem, matrixToCardAvatar) is MatrixToViewEvents.NavigateToRoom -> {
interactionListener?.navigateToRoom(it.roomId)
dismiss()
}
MatrixToViewEvents.Dismiss -> dismiss()
}
}
} }
companion object { companion object {
fun create(matrixItem: MatrixItem, listener: InteractionListener?): MatrixToBottomSheet { fun withLink(matrixToLink: String, listener: InteractionListener?): MatrixToBottomSheet {
return MatrixToBottomSheet(matrixItem).apply { return MatrixToBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, MatrixToBottomSheet.MatrixToArgs(
matrixToLink = matrixToLink
))
}
interactionListener = listener interactionListener = listener
} }
} }

View File

@ -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<MatrixItem> = Uninitialized,
val startChattingState: Async<Unit> = Uninitialized
) : MvRxState {
constructor(args: MatrixToBottomSheet.MatrixToArgs) : this(
deepLink = args.matrixToLink
)
}

View File

@ -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<MatrixToBottomSheetState, MatrixToAction, MatrixToViewEvents>(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<User> {
session.resolveUser(userId, it)
}
}
// Create raw user in case the user is not searchable
?: User(userId, null, null)
}
companion object : MvRxViewModelFactory<MatrixToBottomSheetViewModel, MatrixToBottomSheetState> {
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<String> { 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))
}
}
}
}

View File

@ -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()
}

View File

@ -63,13 +63,14 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
.subscribeOn(Schedulers.computation()) .subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.flatMap { permalinkData -> .flatMap { permalinkData ->
handlePermalink(permalinkData, context, navigationInterceptor, buildTask) handlePermalink(permalinkData, deepLink, context, navigationInterceptor, buildTask)
} }
.onErrorReturnItem(false) .onErrorReturnItem(false)
} }
private fun handlePermalink( private fun handlePermalink(
permalinkData: PermalinkData, permalinkData: PermalinkData,
rawLink: Uri,
context: Context, context: Context,
navigationInterceptor: NavigationInterceptor?, navigationInterceptor: NavigationInterceptor?,
buildTask: Boolean buildTask: Boolean
@ -96,7 +97,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
Single.just(true) Single.just(true)
} }
is PermalinkData.UserLink -> { 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) navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask)
} }
Single.just(true) Single.just(true)
@ -175,7 +176,7 @@ interface NavigationInterceptor {
/** /**
* Return true if the navigation has been intercepted * Return true if the navigation has been intercepted
*/ */
fun navToMemberProfile(userId: String): Boolean { fun navToMemberProfile(userId: String, deepLink: Uri): Boolean {
return false return false
} }
} }

View File

@ -23,11 +23,9 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity 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.home.LoadingFragment
import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
class PermalinkHandlerActivity : VectorBaseActivity() { class PermalinkHandlerActivity : VectorBaseActivity() {
@ -45,23 +43,28 @@ class PermalinkHandlerActivity : VectorBaseActivity() {
if (isFirstCreation()) { if (isFirstCreation()) {
replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java) replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java)
} }
handleIntent()
}
private fun handleIntent() {
// If we are not logged in, open login screen. // If we are not logged in, open login screen.
// In the future, we might want to relaunch the process after login. // In the future, we might want to relaunch the process after login.
if (!sessionHolder.hasActiveSession()) { if (!sessionHolder.hasActiveSession()) {
startLoginActivity() startLoginActivity()
return return
} }
val uri = intent.dataString // We forward intent to HomeActivity (singleTask) to avoid the dueling app problem
permalinkHandler.launch(this, uri, buildTask = true) // https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances
.delay(500, TimeUnit.MILLISECONDS) intent.setClass(this, HomeActivity::class.java)
.observeOn(AndroidSchedulers.mainThread()) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
.subscribe { isHandled -> startActivity(intent)
if (!isHandled) {
toast(R.string.permalink_malformed) finish()
} }
finish()
} override fun onNewIntent(intent: Intent?) {
.disposeOnDestroy() super.onNewIntent(intent)
handleIntent()
} }
private fun startLoginActivity() { private fun startLoginActivity() {

View File

@ -36,7 +36,6 @@ import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_simple.* import kotlinx.android.synthetic.main.activity_simple.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -72,7 +71,7 @@ class UserCodeActivity
UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY) UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY)
is UserCodeState.Mode.RESULT -> { is UserCodeState.Mode.RESULT -> {
showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) 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) { override fun navigateToRoom(roomId: String) {
sharedViewModel.handle(UserCodeActions.StartChattingWithUser(matrixItem)) navigator.openRoom(this, roomId)
} }
override fun onBackPressed() = withState(sharedViewModel) { override fun onBackPressed() = withState(sharedViewModel) {

View File

@ -30,6 +30,7 @@ import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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.raw.RawService
import org.matrix.android.sdk.api.session.Session 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.PermalinkData
@ -72,12 +73,12 @@ class UserCodeSharedViewModel @AssistedInject constructor(
override fun handle(action: UserCodeActions) { override fun handle(action: UserCodeActions) {
when (action) { when (action) {
UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss) UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss)
is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) } is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) }
is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action) is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action)
is UserCodeActions.StartChattingWithUser -> handleStartChatting(action) is UserCodeActions.StartChattingWithUser -> handleStartChatting(action)
UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted) UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted)
UserCodeActions.ShareByText -> handleShareByText() UserCodeActions.ShareByText -> handleShareByText()
} }
} }
@ -139,22 +140,33 @@ class UserCodeSharedViewModel @AssistedInject constructor(
_viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen) _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
when (linkedId) { when (linkedId) {
is PermalinkData.RoomLink -> TODO() is PermalinkData.RoomLink -> {
is PermalinkData.UserLink -> { // not yet supported
val user = session.getUser(linkedId.userId) ?: awaitCallback<List<User>> { _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_implemented)))
session.searchUsersDirectory(linkedId.userId, 10, emptySet(), it) }
}.firstOrNull { it.userId == linkedId.userId } is PermalinkData.UserLink -> {
val user = tryOrNull {
awaitCallback<User> {
session.resolveUser(linkedId.userId, it)
}
}
// Create raw Uxid in case the user is not searchable // Create raw Uxid in case the user is not searchable
?: User(linkedId.userId, null, null) ?: User(linkedId.userId, null, null)
setState { setState {
copy( copy(
mode = UserCodeState.Mode.RESULT(user.toMatrixItem()) mode = UserCodeState.Mode.RESULT(user.toMatrixItem(), action.code)
) )
} }
} }
is PermalinkData.GroupLink -> TODO() is PermalinkData.GroupLink -> {
is PermalinkData.FallbackLink -> TODO() // 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) _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
} }

View File

@ -28,7 +28,7 @@ data class UserCodeState(
sealed class Mode { sealed class Mode {
object SHOW : Mode() object SHOW : Mode()
object SCAN : 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( constructor(args: UserCodeActivity.Args) : this(

View File

@ -3,13 +3,24 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:minHeight="200dp">
<ProgressBar
android:id="@+id/matrixToCardContentLoading"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/matrixToCardAvatar" android:id="@+id/matrixToCardAvatar"
android:layout_width="60dp" android:layout_width="60dp"
android:layout_height="60dp" android:layout_height="60dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="@dimen/layout_vertical_margin_big" android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:elevation="4dp" android:elevation="4dp"
android:transitionName="profile" android:transitionName="profile"
@ -63,4 +74,23 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/matrixToCardUserIdText" /> app:layout_constraintTop_toBottomOf="@id/matrixToCardUserIdText" />
<ProgressBar
android:id="@+id/matrixToCardButtonLoading"
style="?android:attr/progressBarStyleSmall"
android:layout_width="20dp"
android:layout_height="20dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/matrixToCardSendMessageButton"
app:layout_constraintEnd_toEndOf="@id/matrixToCardSendMessageButton"
app:layout_constraintStart_toStartOf="@id/matrixToCardSendMessageButton"
app:layout_constraintTop_toTopOf="@id/matrixToCardSendMessageButton"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/matrixToCardUserContentVisibility"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="matrixToCardAvatar,matrixToCardNameText,matrixToCardUserIdText"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>