Fix issues with matrix.to deep linking

This commit is contained in:
Valere 2020-11-25 15:19:49 +01:00
parent bcd86977d2
commit 804afc9a1d
11 changed files with 511 additions and 58 deletions

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

@ -38,8 +38,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 +54,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 +70,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 +89,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) {
@ -117,9 +125,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
.observe() .observe()
.subscribe { sharedAction -> .subscribe { sharedAction ->
when (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.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> { is HomeActivitySharedAction.OpenGroup -> {
drawerLayout.closeDrawer(GravityCompat.START) drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
} }
@ -136,20 +144,42 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
homeActivityViewModel.observeViewEvents { homeActivityViewModel.observeViewEvents {
when (it) { when (it) {
is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
}.exhaustive }.exhaustive
} }
homeActivityViewModel.subscribe(this) { renderState(it) } homeActivityViewModel.subscribe(this) { renderState(it) }
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) {
when (val status = state.initialSyncProgressServiceStatus) { when (val status = state.initialSyncProgressServiceStatus) {
is InitialSyncProgressService.Status.Idle -> { is InitialSyncProgressService.Status.Idle -> {
waiting_view.isVisible = false waiting_view.isVisible = false
} }
is InitialSyncProgressService.Status.Progressing -> { is InitialSyncProgressService.Status.Progressing -> {
@ -270,6 +300,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 +344,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 +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 { 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

@ -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,38 @@
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?,
val userId: 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 +58,100 @@ 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,
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 interactionListener = listener
} }
} }

View file

@ -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<MatrixItem> = Uninitialized,
val startChattingState: Async<Unit> = Uninitialized
) : MvRxState

View file

@ -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<MatrixToBottomSheetState, MatrixToAction, MatrixToViewEvents>(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<JsonDict> {
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<MatrixToBottomSheetViewModel, MatrixToBottomSheetState> {
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<String> { 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))
}
}
}
}

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

@ -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.withUserId(mode.matrixItem.id, 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,12 +30,15 @@ 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
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser 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.room.model.create.CreateRoomParams
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.JsonDict
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
@ -72,12 +75,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,13 +142,21 @@ 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 -> {
// Create raw Uxid in case the user is not searchable val user = session.getUser(linkedId.userId)
?: User(linkedId.userId, null, null) ?: tryOrNull {
awaitCallback<JsonDict> {
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 { setState {
copy( copy(
@ -153,8 +164,14 @@ class UserCodeSharedViewModel @AssistedInject constructor(
) )
} }
} }
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

@ -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>