Apache License diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt index 12dfcbcaac..ff9865c3ea 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt @@ -37,7 +37,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService: fun setActiveSession(session: Session) { activeSession.set(session) - sessionObservableStore.post(Option.fromNullable(session)) + sessionObservableStore.post(Option.just(session)) keyRequestHandler.start(session) incomingVerificationRequestHandler.start(session) } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 442c5f6f96..c86436d56a 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -47,6 +47,7 @@ import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFr import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment +import im.vector.riotx.features.signout.soft.SoftLogoutFragment @Module interface FragmentModule { @@ -261,4 +262,9 @@ interface FragmentModule { @IntoMap @FragmentKey(EmojiChooserFragment::class) fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SoftLogoutFragment::class) + fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 9f0f83a41f..e0b14af9d0 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentFactory import androidx.lifecycle.ViewModelProvider import dagger.BindsInstance import dagger.Component +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.features.MainActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity @@ -40,6 +41,7 @@ import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.navigation.Navigator +import im.vector.riotx.features.permalink.PermalinkHandlerActivity import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake @@ -49,6 +51,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.IncomingShareActivity +import im.vector.riotx.features.signout.soft.SoftLogoutActivity import im.vector.riotx.features.ui.UiStateRepository @Component( @@ -78,6 +81,8 @@ interface ScreenComponent { fun navigator(): Navigator + fun errorFormatter(): ErrorFormatter + fun uiStateRepository(): UiStateRepository fun inject(activity: HomeActivity) @@ -126,6 +131,10 @@ interface ScreenComponent { fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet) + fun inject(activity: SoftLogoutActivity) + + fun inject(permalinkHandlerActivity: PermalinkHandlerActivity) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index c4b2c40787..b78e291506 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -27,6 +27,7 @@ import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.DimensionConverter @@ -37,6 +38,7 @@ import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.HomeRoomListDataSource import im.vector.riotx.features.home.group.SelectedGroupDataSource import im.vector.riotx.features.html.EventHtmlRenderer +import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.notifications.* import im.vector.riotx.features.rageshake.BugReporter @@ -86,8 +88,12 @@ interface VectorComponent { fun eventHtmlRenderer(): EventHtmlRenderer + fun vectorHtmlCompressor(): VectorHtmlCompressor + fun navigator(): Navigator + fun errorFormatter(): ErrorFormatter + fun homeRoomListObservableStore(): HomeRoomListDataSource fun shareRoomListObservableStore(): ShareRoomListDataSource diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt index 84441d88e1..848c1e0d97 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt @@ -26,6 +26,8 @@ import dagger.Provides import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.session.Session +import im.vector.riotx.core.error.DefaultErrorFormatter +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.features.navigation.DefaultNavigator import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository @@ -72,6 +74,9 @@ abstract class VectorModule { @Binds abstract fun bindNavigator(navigator: DefaultNavigator): Navigator + @Binds + abstract fun bindErrorFormatter(errorFormatter: DefaultErrorFormatter): ErrorFormatter + @Binds abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository } diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/ZeroItem.kt similarity index 50% rename from matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/ZeroItem.kt index d638354dfd..b64abdcc6c 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/ZeroItem.kt @@ -14,25 +14,17 @@ * limitations under the License. */ -package im.vector.matrix.rx +package im.vector.riotx.core.epoxy -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.util.Cancelable -import io.reactivex.SingleEmitter +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R -internal class MatrixCallbackSingle(private val singleEmitter: SingleEmitter ) : MatrixCallback { +/** + * Item of size (0, 0). + * It can be useful to avoid automatic scroll of RecyclerView with Epoxy controller, when the first valuable item changes. + */ +@EpoxyModelClass(layout = R.layout.item_zero) +abstract class ZeroItem : VectorEpoxyModel () { - override fun onSuccess(data: T) { - singleEmitter.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - singleEmitter.tryOnError(failure) - } -} - -fun Cancelable.toSingle(singleEmitter: SingleEmitter ) { - singleEmitter.setCancellable { - this.cancel() - } + class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 8105d7a7c0..7b79ce8549 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -21,6 +21,7 @@ import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -37,11 +38,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel null is Failure.NetworkConnection -> { @@ -41,6 +42,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi stringProvider.getString(R.string.error_network_timeout) throwable.ioException is UnknownHostException -> // Invalid homeserver? + // TODO Check network state, airplane mode, etc. stringProvider.getString(R.string.login_error_unknown_host) else -> stringProvider.getString(R.string.error_no_network) @@ -52,23 +54,23 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi // Special case for terms and conditions stringProvider.getString(R.string.error_terms_not_accepted) } - throwable.error.code == MatrixError.FORBIDDEN + throwable.error.code == MatrixError.M_FORBIDDEN && throwable.error.message == "Invalid password" -> { stringProvider.getString(R.string.auth_invalid_login_param) } - throwable.error.code == MatrixError.USER_IN_USE -> { + throwable.error.code == MatrixError.M_USER_IN_USE -> { stringProvider.getString(R.string.login_signup_error_user_in_use) } - throwable.error.code == MatrixError.BAD_JSON -> { + throwable.error.code == MatrixError.M_BAD_JSON -> { stringProvider.getString(R.string.login_error_bad_json) } - throwable.error.code == MatrixError.NOT_JSON -> { + throwable.error.code == MatrixError.M_NOT_JSON -> { stringProvider.getString(R.string.login_error_not_json) } - throwable.error.code == MatrixError.LIMIT_EXCEEDED -> { + throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { limitExceededError(throwable.error) } - throwable.error.code == MatrixError.THREEPID_NOT_FOUND -> { + throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { stringProvider.getString(R.string.login_reset_password_error_not_found) } else -> { diff --git a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt b/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt index dd4257fe1f..614340bd3d 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt @@ -21,6 +21,6 @@ import im.vector.matrix.android.api.failure.MatrixError import javax.net.ssl.HttpsURLConnection fun Throwable.is401(): Boolean { - return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && this.error.code == MatrixError.UNAUTHORIZED) + return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && error.code == MatrixError.M_UNAUTHORIZED) } diff --git a/vector/src/main/java/im/vector/riotx/core/error/fatal.kt b/vector/src/main/java/im/vector/riotx/core/error/fatal.kt new file mode 100644 index 0000000000..800e1ea7ad --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/error/fatal.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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.riotx.core.error + +import im.vector.riotx.BuildConfig +import timber.log.Timber + +/** + * throw in debug, only log in production. As this method does not always throw, next statement should be a return + */ +fun fatalError(message: String) { + if (BuildConfig.DEBUG) { + error(message) + } else { + Timber.e(message) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt index 0ce4d04497..67e866bb82 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.extensions import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.sync.FilterService import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener @@ -40,3 +41,11 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener, // @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler // @Inject lateinit var keyRequestHandler: KeyRequestHandler } + +/** + * Tell is the session has unsaved e2e keys in the backup + */ +fun Session.hasUnsavedKeys(): Boolean { + return inboundGroupSessionsCount(false) > 0 + && getKeysBackupService().state != KeysBackupState.ReadyToBackUp +} diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt index 9a26cedf9a..7614fda619 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt @@ -35,3 +35,12 @@ fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder return this } + +/** + * Ex: "https://matrix.org/" -> "matrix.org" + */ +fun String?.toReducedUrl(): String { + return (this ?: "") + .substringAfter("://") + .trim { it == '/' } +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 79b040cd41..f70aed9393 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -38,12 +38,15 @@ import butterknife.Unbinder import com.airbnb.mvrx.MvRx import com.bumptech.glide.util.Util import com.google.android.material.snackbar.Snackbar +import im.vector.matrix.android.api.failure.GlobalError import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.* import im.vector.riotx.core.dialogs.DialogLocker import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.consent.ConsentNotGivenHelper import im.vector.riotx.features.navigation.Navigator @@ -89,6 +92,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { protected lateinit var navigator: Navigator private lateinit var activeSessionHolder: ActiveSessionHolder + // Filter for multiple invalid token error + private var mainActivityStarted = false + private var unBinder: Unbinder? = null private var savedInstanceState: Bundle? = null @@ -153,9 +159,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { }) sessionListener = getVectorComponent().sessionListener() - sessionListener.consentNotGivenLiveData.observeEvent(this) { - consentNotGivenHelper.displayDialog(it.consentUri, - activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "") + sessionListener.globalErrorLiveData.observeEvent(this) { + handleGlobalError(it) } doBeforeSetContentView() @@ -180,6 +185,33 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } } + private fun handleGlobalError(globalError: GlobalError) { + when (globalError) { + is GlobalError.InvalidToken -> + handleInvalidToken(globalError) + is GlobalError.ConsentNotGivenError -> + consentNotGivenHelper.displayDialog(globalError.consentUri, + activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "") + } + } + + protected open fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + Timber.w("Invalid token event received") + if (mainActivityStarted) { + return + } + + mainActivityStarted = true + + MainActivity.restartApp(this, + MainActivityArgs( + clearCredentials = !globalError.softLogout, + isUserLoggedOut = true, + isSoftLogout = globalError.softLogout + ) + ) + } + override fun onDestroy() { super.onDestroy() unBinder?.unbind() @@ -190,8 +222,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { override fun onResume() { super.onResume() - - Timber.v("onResume Activity ${this.javaClass.simpleName}") + Timber.i("onResume Activity ${this.javaClass.simpleName}") configurationViewModel.onActivityResumed() diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index 70311e2f57..b3a56f48ee 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -32,6 +32,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.utils.DimensionConverter +import timber.log.Timber /** * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) @@ -80,6 +81,11 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() super.onCreate(savedInstanceState) } + override fun onResume() { + super.onResume() + Timber.i("onResume BottomSheet ${this.javaClass.simpleName}") + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return super.onCreateDialog(savedInstanceState).apply { val dialog = this as? BottomSheetDialog diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index 924cb6c7bc..8e1ad72150 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -34,6 +34,7 @@ import com.bumptech.glide.util.Util.assertMainThread import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.features.navigation.Navigator import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable @@ -49,12 +50,14 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { } /* ========================================================================================== - * Navigator + * Navigator and other common objects * ========================================================================================== */ - protected lateinit var navigator: Navigator private lateinit var screenComponent: ScreenComponent + protected lateinit var navigator: Navigator + protected lateinit var errorFormatter: ErrorFormatter + /* ========================================================================================== * View model * ========================================================================================== */ @@ -74,6 +77,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) navigator = screenComponent.navigator() + errorFormatter = screenComponent.errorFormatter() viewModelFactory = screenComponent.viewModelFactory() childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() injectWith(injector()) @@ -100,7 +104,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { @CallSuper override fun onResume() { super.onResume() - Timber.v("onResume Fragment ${this.javaClass.simpleName}") + Timber.i("onResume Fragment ${this.javaClass.simpleName}") } @CallSuper diff --git a/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt b/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt index a2c3e90910..c7fcf85a16 100755 --- a/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt +++ b/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt @@ -23,6 +23,8 @@ import android.widget.ProgressBar import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.home.AvatarRenderer @@ -59,9 +61,9 @@ open class UserAvatarPreference : Preference { val session = mSession ?: return val view = mAvatarView ?: return session.getUser(session.myUserId)?.let { - avatarRenderer.render(it, view) + avatarRenderer.render(it.toMatrixItem(), view) } ?: run { - avatarRenderer.render(null, session.myUserId, null, view) + avatarRenderer.render(MatrixItem.UserItem(session.myUserId), view) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt b/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt similarity index 79% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt rename to vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt index 80ee6811bb..65ab0ad2b2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt @@ -14,9 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.failure +package im.vector.riotx.core.ui.model -// This data class will be sent to the bus -data class ConsentNotGivenError( - val consentUri: String -) +// android.util.Size in API 21+ +data class Size(val width: Int, val height: Int) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index 6e4229908f..c5e2fdf375 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -26,6 +26,7 @@ import im.vector.riotx.R import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.riotx.features.home.room.detail.timeline.item.toMatrixItem import kotlinx.android.synthetic.main.view_read_receipts.view.* private const val MAX_RECEIPT_DISPLAYED = 5 @@ -59,7 +60,7 @@ class ReadReceiptsView @JvmOverloads constructor( receiptAvatars[index].visibility = View.INVISIBLE } else { receiptAvatars[index].visibility = View.VISIBLE - avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index]) + avatarRenderer.render(receiptData.toMatrixItem(), receiptAvatars[index]) } } diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index 7064ad0d49..041eb85a11 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -19,9 +19,11 @@ package im.vector.riotx.features import android.app.Activity import android.content.Intent import android.os.Bundle +import android.os.Parcelable import androidx.appcompat.app.AlertDialog import com.bumptech.glide.Glide import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.failure.GlobalError import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent @@ -30,6 +32,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.deleteAllFiles import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.LoginActivity +import im.vector.riotx.features.notifications.NotificationDrawerManager +import im.vector.riotx.features.signout.hard.SignedOutActivity +import im.vector.riotx.features.signout.soft.SoftLogoutActivity +import kotlinx.android.parcel.Parcelize import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -37,23 +43,37 @@ import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject +@Parcelize +data class MainActivityArgs( + val clearCache: Boolean = false, + val clearCredentials: Boolean = false, + val isUserLoggedOut: Boolean = false, + val isSoftLogout: Boolean = false +) : Parcelable + +/** + * This is the entry point of RiotX + * This Activity, when started with argument, is also doing some cleanup when user disconnects, + * clears cache, is logged out, or is soft logged out + */ class MainActivity : VectorBaseActivity() { companion object { - private const val EXTRA_CLEAR_CACHE = "EXTRA_CLEAR_CACHE" - private const val EXTRA_CLEAR_CREDENTIALS = "EXTRA_CLEAR_CREDENTIALS" + private const val EXTRA_ARGS = "EXTRA_ARGS" // Special action to clear cache and/or clear credentials - fun restartApp(activity: Activity, clearCache: Boolean = false, clearCredentials: Boolean = false) { + fun restartApp(activity: Activity, args: MainActivityArgs) { val intent = Intent(activity, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - intent.putExtra(EXTRA_CLEAR_CACHE, clearCache) - intent.putExtra(EXTRA_CLEAR_CREDENTIALS, clearCredentials) + intent.putExtra(EXTRA_ARGS, args) activity.startActivity(intent) } } + private lateinit var args: MainActivityArgs + + @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var errorFormatter: ErrorFormatter @@ -63,42 +83,71 @@ class MainActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val clearCache = intent.getBooleanExtra(EXTRA_CLEAR_CACHE, false) - val clearCredentials = intent.getBooleanExtra(EXTRA_CLEAR_CREDENTIALS, false) + args = parseArgs() + + if (args.clearCredentials || args.isUserLoggedOut) { + clearNotifications() + } // Handle some wanted cleanup - if (clearCache || clearCredentials) { - doCleanUp(clearCache, clearCredentials) + if (args.clearCache || args.clearCredentials) { + doCleanUp() } else { - start() + startNextActivityAndFinish() } } - private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) { + private fun clearNotifications() { + // Dismiss all notifications + notificationDrawerManager.clearAllEvents() + notificationDrawerManager.persistInfo() + } + + private fun parseArgs(): MainActivityArgs { + val argsFromIntent: MainActivityArgs? = intent.getParcelableExtra(EXTRA_ARGS) + Timber.w("Starting MainActivity with $argsFromIntent") + + return MainActivityArgs( + clearCache = argsFromIntent?.clearCache ?: false, + clearCredentials = argsFromIntent?.clearCredentials ?: false, + isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false, + isSoftLogout = argsFromIntent?.isSoftLogout ?: false + ) + } + + private fun doCleanUp() { when { - clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.w("SIGN_OUT: success, start app") - sessionHolder.clearActiveSession() - doLocalCleanupAndStart() - } + args.clearCredentials -> sessionHolder.getActiveSession().signOut( + !args.isUserLoggedOut, + object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.w("SIGN_OUT: success, start app") + sessionHolder.clearActiveSession() + doLocalCleanupAndStart() + } - override fun onFailure(failure: Throwable) { - displayError(failure, clearCache, clearCredentials) - } - }) - clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback { - override fun onSuccess(data: Unit) { - doLocalCleanupAndStart() - } + override fun onFailure(failure: Throwable) { + displayError(failure) + } + }) + args.clearCache -> sessionHolder.getActiveSession().clearCache( + object : MatrixCallback { + override fun onSuccess(data: Unit) { + doLocalCleanupAndStart() + } - override fun onFailure(failure: Throwable) { - displayError(failure, clearCache, clearCredentials) - } - }) + override fun onFailure(failure: Throwable) { + displayError(failure) + } + }) } } + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + // No op here + Timber.w("Ignoring invalid token global error") + } + private fun doLocalCleanupAndStart() { GlobalScope.launch(Dispatchers.Main) { // On UI Thread @@ -112,24 +161,43 @@ class MainActivity : VectorBaseActivity() { } } - start() + startNextActivityAndFinish() } - private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) { + private fun displayError(failure: Throwable) { AlertDialog.Builder(this) .setTitle(R.string.dialog_title_error) .setMessage(errorFormatter.toHumanReadable(failure)) - .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) } - .setNegativeButton(R.string.cancel) { _, _ -> start() } + .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } + .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() } .setCancelable(false) .show() } - private fun start() { - val intent = if (sessionHolder.hasActiveSession()) { - HomeActivity.newIntent(this) - } else { - LoginActivity.newIntent(this, null) + private fun startNextActivityAndFinish() { + val intent = when { + args.clearCredentials + && !args.isUserLoggedOut -> + // User has explicitly asked to log out + LoginActivity.newIntent(this, null) + args.isSoftLogout -> + // The homeserver has invalidated the token, with a soft logout + SoftLogoutActivity.newIntent(this) + args.isUserLoggedOut -> + // the homeserver has invalidated the token (password changed, device deleted, other security reason + SignedOutActivity.newIntent(this) + sessionHolder.hasActiveSession() -> + // We have a session. + // Check it can be opened + if (sessionHolder.getActiveSession().isOpenable) { + HomeActivity.newIntent(this) + } else { + // The token is still invalid + SoftLogoutActivity.newIntent(this) + } + else -> + // First start, or no active session + LoginActivity.newIntent(this, null) } startActivity(intent) finish() diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt index 5e843fcdfd..4b51c548a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/AttachmentsMapper.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.attachments import com.kbeanie.multipicker.api.entity.* import im.vector.matrix.android.api.session.content.ContentAttachmentData +import timber.log.Timber fun ChosenContact.toContactAttachment(): ContactAttachment { return ContactAttachment( @@ -29,6 +30,7 @@ fun ChosenContact.toContactAttachment(): ContactAttachment { } fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { + if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( path = originalPath, mimeType = mimeType, @@ -40,6 +42,7 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData { } fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { + if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( path = originalPath, mimeType = mimeType, @@ -51,16 +54,17 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData { ) } -fun ChosenFile.mapType(): ContentAttachmentData.Type { +private fun ChosenFile.mapType(): ContentAttachmentData.Type { return when { - mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE - mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO - mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO - else -> ContentAttachmentData.Type.FILE + mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE + mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO + mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO + else -> ContentAttachmentData.Type.FILE } } fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { + if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( path = originalPath, mimeType = mimeType, @@ -75,6 +79,7 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData { } fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData { + if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( path = originalPath, mimeType = mimeType, diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt index 01b6bdd41a..8f0090001f 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt @@ -18,11 +18,12 @@ package im.vector.riotx.features.autocomplete.user import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class AutocompleteUserController @Inject constructor(): TypedEpoxyController >() { +class AutocompleteUserController @Inject constructor() : TypedEpoxyController
>() { var listener: AutocompleteClickListener
? = null @@ -35,9 +36,7 @@ class AutocompleteUserController @Inject constructor(): TypedEpoxyController autocompleteUserItem { id(user.userId) - userId(user.userId) - name(user.displayName) - avatarUrl(user.avatarUrl) + matrixItem(user.toMatrixItem()) avatarRenderer(avatarRenderer) clickListener { _ -> listener?.onItemClick(user) diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt index b32562d8e6..8581ba8e2c 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt @@ -21,6 +21,7 @@ import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -30,15 +31,13 @@ import im.vector.riotx.features.home.AvatarRenderer abstract class AutocompleteUserItem : VectorEpoxyModel () { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute var name: String? = null - @EpoxyAttribute var userId: String = "" - @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var clickListener: View.OnClickListener? = null override fun bind(holder: Holder) { holder.view.setOnClickListener(clickListener) - holder.nameView.text = name - avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) + holder.nameView.text = matrixItem.getBestName() + avatarRenderer.render(matrixItem, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt index 88df53d0f3..61f5c5f9fe 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt @@ -22,6 +22,8 @@ import androidx.lifecycle.Observer import butterknife.BindView import butterknife.OnClick import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.home.AvatarRenderer @@ -57,10 +59,10 @@ class SASVerificationIncomingFragment @Inject constructor( otherDeviceTextView.text = viewModel.otherDeviceId viewModel.otherUser?.let { - avatarRenderer.render(it, avatarImageView) + avatarRenderer.render(it.toMatrixItem(), avatarImageView) } ?: run { // Fallback to what we know - avatarRenderer.render(null, viewModel.otherUserId ?: "", viewModel.otherUserId, avatarImageView) + avatarRenderer.render(MatrixItem.UserItem(viewModel.otherUserId ?: "", viewModel.otherUserId), avatarImageView) } viewModel.transactionState.observe(viewLifecycleOwner, Observer { diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 9975ee91cd..4e1808a48a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -27,10 +27,7 @@ import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target import im.vector.matrix.android.api.session.content.ContentUrlResolver -import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.user.model.User -import im.vector.matrix.android.internal.util.firstLetterOfDisplayName -import im.vector.riotx.R +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideRequest @@ -45,76 +42,42 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active companion object { private const val THUMBNAIL_SIZE = 250 - - private val AVATAR_COLOR_LIST = listOf( - R.color.riotx_avatar_fill_1, - R.color.riotx_avatar_fill_2, - R.color.riotx_avatar_fill_3 - ) } @UiThread - fun render(roomSummary: RoomSummary, imageView: ImageView) { - render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView) - } - - @UiThread - fun render(user: User, imageView: ImageView) { - render(imageView.context, GlideApp.with(imageView), user.avatarUrl, user.userId, user.displayName, DrawableImageViewTarget(imageView)) - } - - @UiThread - fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) { - render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView)) + fun render(matrixItem: MatrixItem, imageView: ImageView) { + render(imageView.context, + GlideApp.with(imageView), + matrixItem, + DrawableImageViewTarget(imageView)) } @UiThread fun render(context: Context, glideRequest: GlideRequests, - avatarUrl: String?, - identifier: String, - name: String?, + matrixItem: MatrixItem, target: Target ) { - val displayName = if (name.isNullOrBlank()) { - identifier - } else { - name - } - val placeholder = getPlaceholderDrawable(context, identifier, displayName) - buildGlideRequest(glideRequest, avatarUrl) + val placeholder = getPlaceholderDrawable(context, matrixItem) + buildGlideRequest(glideRequest, matrixItem.avatarUrl) .placeholder(placeholder) .into(target) } @AnyThread - fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable { - val avatarColor = ContextCompat.getColor(context, getColorFromUserId(identifier)) - return if (text.isEmpty()) { - TextDrawable.builder().buildRound("", avatarColor) - } else { - val firstLetter = text.firstLetterOfDisplayName() - TextDrawable.builder() - .beginConfig() - .bold() - .endConfig() - .buildRound(firstLetter, avatarColor) + fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { + val avatarColor = when (matrixItem) { + is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) + else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) } + return TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor) } // PRIVATE API ********************************************************************************* -// private fun getAvatarColor(text: String? = null): Int { -// var colorIndex: Long = 0 -// if (!text.isNullOrEmpty()) { -// var sum: Long = 0 -// for (i in 0 until text.length) { -// sum += text[i].toLong() -// } -// colorIndex = sum % AVATAR_COLOR_LIST.size -// } -// return AVATAR_COLOR_LIST[colorIndex.toInt()] -// } - private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver() .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index ac8d429cb1..fc0eeaf92c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -27,6 +27,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.platform.ToolbarConfigurable @@ -74,12 +75,7 @@ class HomeDetailFragment @Inject constructor( private fun onGroupChange(groupSummary: GroupSummary?) { groupSummary?.let { - avatarRenderer.render( - it.avatarUrl, - it.groupId, - it.displayName, - groupToolbarAvatarImageView - ) + avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView) } } @@ -155,7 +151,7 @@ class HomeDetailFragment @Inject constructor( bottomNavigationView.selectedItemId = when (displayMode) { RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms - else -> R.id.bottom_action_home + else -> R.id.bottom_action_home } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt index c7c5e4a233..1777fa03c1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt @@ -34,5 +34,5 @@ data class HomeDetailViewState( val notificationHighlightPeople: Boolean = false, val notificationCountRooms: Int = 0, val notificationHighlightRooms: Boolean = false, - val syncState: SyncState = SyncState.IDLE + val syncState: SyncState = SyncState.Idle ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index 422b59671e..6ff836e8c8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home import android.os.Bundle import android.view.View import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.extensions.observeK import im.vector.riotx.core.extensions.replaceChildFragment @@ -42,7 +43,7 @@ class HomeDrawerFragment @Inject constructor( session.liveUser(session.myUserId).observeK(this) { optionalUser -> val user = optionalUser?.getOrNull() if (user != null) { - avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) + avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView) homeDrawerUsernameView.text = user.displayName homeDrawerUserIdView.text = user.userId } diff --git a/vector/src/main/java/im/vector/riotx/features/home/PermalinkHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/PermalinkHandler.kt deleted file mode 100644 index 00bcd87253..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/PermalinkHandler.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home - -import android.content.Context -import android.net.Uri -import im.vector.matrix.android.api.permalinks.PermalinkData -import im.vector.matrix.android.api.permalinks.PermalinkParser -import im.vector.matrix.android.api.session.Session -import im.vector.riotx.features.navigation.Navigator -import javax.inject.Inject - -class PermalinkHandler @Inject constructor(private val session: Session, - private val navigator: Navigator) { - - fun launch(context: Context, deepLink: String?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean { - val uri = deepLink?.let { Uri.parse(it) } - return launch(context, uri, navigateToRoomInterceptor) - } - - fun launch(context: Context, deepLink: Uri?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean { - if (deepLink == null) { - return false - } - - return when (val permalinkData = PermalinkParser.parse(deepLink)) { - is PermalinkData.EventLink -> { - if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias, permalinkData.eventId) != true) { - openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId) - } - - true - } - is PermalinkData.RoomLink -> { - if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias) != true) { - openRoom(context, permalinkData.roomIdOrAlias) - } - - true - } - is PermalinkData.GroupLink -> { - navigator.openGroupDetail(permalinkData.groupId, context) - true - } - is PermalinkData.UserLink -> { - navigator.openUserDetail(permalinkData.userId, context) - true - } - is PermalinkData.FallbackLink -> { - false - } - } - } - - /** - * Open room either joined, or not unknown - */ - private fun openRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) { - if (session.getRoom(roomIdOrAlias) != null) { - navigator.openRoom(context, roomIdOrAlias, eventId) - } else { - navigator.openNotJoinedRoom(context, roomIdOrAlias, eventId) - } - } -} - -interface NavigateToRoomInterceptor { - - /** - * Return true if the navigation has been intercepted - */ - fun navToRoom(roomId: String, eventId: String? = null): Boolean -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/RoomColor.kt b/vector/src/main/java/im/vector/riotx/features/home/RoomColor.kt new file mode 100644 index 0000000000..0b3fd5396f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/RoomColor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.riotx.features.home + +import androidx.annotation.ColorRes +import im.vector.riotx.R + +@ColorRes +fun getColorFromRoomId(roomId: String?): Int { + return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) { + 1 -> R.color.riotx_avatar_fill_2 + 2 -> R.color.riotx_avatar_fill_3 + else -> R.color.riotx_avatar_fill_1 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt b/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt index a88299cc25..d34ca6506a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt @@ -22,28 +22,18 @@ import kotlin.math.abs @ColorRes fun getColorFromUserId(userId: String?): Int { - if (userId.isNullOrBlank()) { - return R.color.riotx_username_1 - } - var hash = 0 - var i = 0 - var chr: Char - while (i < userId.length) { - chr = userId[i] - hash = (hash shl 5) - hash + chr.toInt() - i++ - } + userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() } - return when (abs(hash) % 8 + 1) { - 1 -> R.color.riotx_username_1 - 2 -> R.color.riotx_username_2 - 3 -> R.color.riotx_username_3 - 4 -> R.color.riotx_username_4 - 5 -> R.color.riotx_username_5 - 6 -> R.color.riotx_username_6 - 7 -> R.color.riotx_username_7 - else -> R.color.riotx_username_8 + return when (abs(hash) % 8) { + 1 -> R.color.riotx_username_2 + 2 -> R.color.riotx_username_3 + 3 -> R.color.riotx_username_4 + 4 -> R.color.riotx_username_5 + 5 -> R.color.riotx_username_6 + 6 -> R.color.riotx_username_7 + 7 -> R.color.riotx_username_8 + else -> R.color.riotx_username_1 } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt index 0ff4c5baf8..401d4445fe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.amulyakhare.textdrawable.TextDrawable +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -34,22 +35,20 @@ import im.vector.riotx.features.home.AvatarRenderer abstract class CreateDirectRoomUserItem : VectorEpoxyModel () { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute var name: String? = null - @EpoxyAttribute var userId: String = "" - @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var selected: Boolean = false override fun bind(holder: Holder) { holder.view.setOnClickListener(clickListener) // If name is empty, use userId as name and force it being centered - if (name.isNullOrEmpty()) { + if (matrixItem.displayName.isNullOrEmpty()) { holder.userIdView.visibility = View.GONE - holder.nameView.text = userId + holder.nameView.text = matrixItem.id } else { holder.userIdView.visibility = View.VISIBLE - holder.nameView.text = name - holder.userIdView.text = userId + holder.nameView.text = matrixItem.displayName + holder.userIdView.text = matrixItem.id } renderSelection(holder, selected) } @@ -62,7 +61,7 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel - users.sortedBy { it.displayName.firstLetterOfDisplayName() } + users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } } } stream.toAsync { diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt index 265a38b2c9..8d2b3928be 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt @@ -19,9 +19,13 @@ package im.vector.riotx.features.home.createdirect import com.airbnb.epoxy.EpoxyController -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem @@ -94,9 +98,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session, createDirectRoomUserItem { id(user.userId) selected(isSelected) - userId(user.userId) - name(user.displayName) - avatarUrl(user.avatarUrl) + matrixItem(user.toMatrixItem()) avatarRenderer(avatarRenderer) clickListener { _ -> callback?.onItemClick(user) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt index 3d1ee84254..8270683975 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt @@ -23,7 +23,7 @@ import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.user.model.User -import im.vector.matrix.android.internal.util.firstLetterOfDisplayName +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.EmptyItem_ import im.vector.riotx.core.epoxy.loadingItem @@ -68,9 +68,7 @@ class KnownUsersController @Inject constructor(private val session: Session, CreateDirectRoomUserItem_() .id(item.userId) .selected(isSelected) - .userId(item.userId) - .name(item.displayName) - .avatarUrl(item.avatarUrl) + .matrixItem(item.toMatrixItem()) .avatarRenderer(avatarRenderer) .clickListener { _ -> callback?.onItemClick(item) @@ -87,8 +85,8 @@ class KnownUsersController @Inject constructor(private val session: Session, var lastFirstLetter: String? = null for (model in models) { if (model is CreateDirectRoomUserItem) { - if (model.userId == session.myUserId) continue - val currentFirstLetter = model.name.firstLetterOfDisplayName() + if (model.matrixItem.id == session.myUserId) continue + val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName() val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter lastFirstLetter = currentFirstLetter diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index d9a38d5d9b..24318bc508 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -36,7 +36,7 @@ import im.vector.riotx.core.utils.LiveEvent import io.reactivex.Observable import io.reactivex.functions.BiFunction -const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID" +const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID" class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState, private val selectedGroupStore: SelectedGroupDataSource, @@ -68,14 +68,14 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro } private fun observeSelectionState() { - selectSubscribe(GroupListViewState::selectedGroup) { - if (it != null) { + selectSubscribe(GroupListViewState::selectedGroup) { groupSummary -> + if (groupSummary != null) { val selectedGroup = _openGroupLiveData.value?.peekContent() - // We only wan to open group if the updated selectedGroup is a different one. - if (selectedGroup?.groupId != it.groupId) { - _openGroupLiveData.postLiveEvent(it) + // We only want to open group if the updated selectedGroup is a different one. + if (selectedGroup?.groupId != groupSummary.groupId) { + _openGroupLiveData.postLiveEvent(groupSummary) } - val optionGroup = Option.fromNullable(it) + val optionGroup = Option.just(groupSummary) selectedGroupStore.post(optionGroup) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt index 7c3cfd2a94..95054d1689 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.group import com.airbnb.epoxy.EpoxyController import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject @@ -49,10 +50,8 @@ class GroupSummaryController @Inject constructor(private val avatarRenderer: Ava groupSummaryItem { avatarRenderer(avatarRenderer) id(groupSummary.groupId) - groupId(groupSummary.groupId) - groupName(groupSummary.displayName) + matrixItem(groupSummary.toMatrixItem()) selected(isSelected) - avatarUrl(groupSummary.avatarUrl) listener { callback?.onGroupSelected(groupSummary) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt index 30c1852f1d..61c589cc00 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt @@ -20,6 +20,7 @@ import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -30,18 +31,16 @@ import im.vector.riotx.features.home.AvatarRenderer abstract class GroupSummaryItem : VectorEpoxyModel () { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute lateinit var groupName: CharSequence - @EpoxyAttribute lateinit var groupId: String - @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null override fun bind(holder: Holder) { super.bind(holder) holder.rootView.setOnClickListener { listener?.invoke() } - holder.groupNameView.text = groupName + holder.groupNameView.text = matrixItem.displayName holder.rootView.isChecked = selected - avatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView) + avatarRenderer.render(matrixItem, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt index 3e400b37ea..3b77835917 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt @@ -18,6 +18,8 @@ package im.vector.riotx.features.home.room.breadcrumbs import android.view.View import com.airbnb.epoxy.EpoxyController +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.epoxy.zeroItem import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject @@ -44,17 +46,19 @@ class BreadcrumbsController @Inject constructor( override fun buildModels() { val safeViewState = viewState ?: return + // Add a ZeroItem to avoid automatic scroll when the breadcrumbs are updated from another client + zeroItem { + id("top") + } + // An empty breadcrumbs list can only be temporary because when entering in a room, // this one is added to the breadcrumbs - safeViewState.asyncBreadcrumbs.invoke() ?.forEach { breadcrumbsItem { id(it.roomId) avatarRenderer(avatarRenderer) - roomId(it.roomId) - roomName(it.displayName) - avatarUrl(it.avatarUrl) + matrixItem(it.toMatrixItem()) unreadNotificationCount(it.notificationCount) showHighlighted(it.highlightCount > 0) hasUnreadMessage(it.hasUnreadMessages) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt index b8e2cf7987..5407c73f35 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt @@ -48,6 +48,7 @@ class BreadcrumbsFragment @Inject constructor( override fun onDestroyView() { breadcrumbsRecyclerView.cleanup() + breadcrumbsController.listener = null super.onDestroyView() } @@ -56,6 +57,7 @@ class BreadcrumbsFragment @Inject constructor( breadcrumbsController.listener = this } + // TODO Use invalidate() ? private fun renderState(state: BreadcrumbsViewState) { breadcrumbsController.update(state) } @@ -65,4 +67,8 @@ class BreadcrumbsFragment @Inject constructor( override fun onBreadcrumbClicked(roomId: String) { sharedActionViewModel.post(RoomDetailSharedAction.SwitchToRoom(roomId)) } + + fun scrollToTop() { + breadcrumbsRecyclerView.scrollToPosition(0) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt index 074c35af00..6d18a85b75 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt @@ -22,6 +22,7 @@ import android.widget.ImageView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -32,9 +33,7 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView abstract class BreadcrumbsItem : VectorEpoxyModel () { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute lateinit var roomId: String - @EpoxyAttribute lateinit var roomName: CharSequence - @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var hasUnreadMessage: Boolean = false @@ -45,7 +44,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel () { super.bind(holder) holder.rootView.setOnClickListener(itemClickListener) holder.unreadIndentIndicator.isVisible = hasUnreadMessage - avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) + avatarRenderer.render(matrixItem, holder.avatarImageView) holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) holder.draftIndentIndicator.isVisible = hasDraft } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index 431c9e6395..14e9061c36 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -86,9 +86,19 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { hideKeyboard() + + if (!drawerLayout.isDrawerOpen(GravityCompat.START) && newState == DrawerLayout.STATE_DRAGGING) { + // User is starting to open the drawer, scroll the list to op + scrollBreadcrumbsToTop() + } } } + private fun scrollBreadcrumbsToTop() { + supportFragmentManager.fragments.filterIsInstance () + .forEach { it.scrollToTop() } + } + override fun onBackPressed() { if (drawerLayout.isDrawerOpen(GravityCompat.START)) { drawerLayout.closeDrawer(GravityCompat.START) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 80f54a9c1f..49f23f7f2c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -66,10 +66,11 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.* import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp @@ -85,8 +86,6 @@ import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.NavigateToRoomInterceptor -import im.vector.riotx.features.home.PermalinkHandler import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.composer.TextComposerAction import im.vector.riotx.features.home.room.detail.composer.TextComposerView @@ -108,10 +107,14 @@ import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.notifications.NotificationDrawerManager +import im.vector.riotx.features.permalink.NavigateToRoomInterceptor +import im.vector.riotx.features.permalink.PermalinkHandler import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.themes.ThemeUtils +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* @@ -141,7 +144,6 @@ class RoomDetailFragment @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val textComposerViewModelFactory: TextComposerViewModel.Factory, - private val errorFormatter: ErrorFormatter, private val eventHtmlRenderer: EventHtmlRenderer, private val vectorPreferences: VectorPreferences ) : @@ -410,14 +412,14 @@ class RoomDetailFragment @Inject constructor( composerLayout.sendButton.setContentDescription(getString(descriptionRes)) avatarRenderer.render( - event.senderAvatar, - event.root.senderId ?: "", - event.getDisambiguatedDisplayName(), + MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), composerLayout.composerRelatedMessageAvatar ) composerLayout.expand { - // need to do it here also when not using quick reply - focusComposerAndShowKeyboard() + if (isAdded) { + // need to do it here also when not using quick reply + focusComposerAndShowKeyboard() + } } focusComposerAndShowKeyboard() } @@ -601,20 +603,19 @@ class RoomDetailFragment @Inject constructor( } // Replace the word by its completion - val displayName = item.displayName ?: item.userId + val matrixItem = item.toMatrixItem() + val displayName = matrixItem.getBestName() // with a trailing space editable.replace(startIndex, endIndex, "$displayName ") // Add the span - val user = session.getUser(item.userId) val span = PillImageSpan( glideRequests, avatarRenderer, requireContext(), - item.userId, - user?.displayName ?: item.userId, - user?.avatarUrl) + matrixItem + ) span.bind(composerLayout.composerEditText) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -686,7 +687,7 @@ class RoomDetailFragment @Inject constructor( inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) - avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) + avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView) } else if (summary?.membership == Membership.INVITE && inviter != null) { inviteView.visibility = View.VISIBLE inviteView.render(inviter, VectorInviteView.Mode.LARGE) @@ -713,7 +714,7 @@ class RoomDetailFragment @Inject constructor( activity?.finish() } else { roomToolbarTitleView.text = it.displayName - avatarRenderer.render(it, roomToolbarAvatarImageView) + avatarRenderer.render(it.toMatrixItem(), roomToolbarAvatarImageView) roomToolbarSubtitleView.setTextOrHide(it.topic) } jumpToBottomView.count = it.notificationCount @@ -854,30 +855,33 @@ class RoomDetailFragment @Inject constructor( // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { - val managed = permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - // Same room? - if (roomId == roomDetailArgs.roomId) { - // Navigation to same room - if (eventId == null) { - showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) - } else { - // Highlight and scroll to this event - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + permalinkHandler + .launch(requireActivity(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String?, eventId: String?): Boolean { + // Same room? + if (roomId == roomDetailArgs.roomId) { + // Navigation to same room + if (eventId == null) { + showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) + } else { + // Highlight and scroll to this event + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + } + return true + } + // Not handled + return false + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { managed -> + if (!managed) { + // Open in external browser, in a new Tab + openUrlInExternalBrowser(requireContext(), url) } - return true } - - // Not handled - return false - } - }) - - if (!managed) { - // Open in external browser, in a new Tab - openUrlInExternalBrowser(requireContext(), url) - } - + .disposeOnDestroyView() // In fact it is always managed return true } @@ -1025,12 +1029,15 @@ class RoomDetailFragment @Inject constructor( } override fun onRoomCreateLinkClicked(url: String) { - permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - requireActivity().finish() - return false - } - }) + permalinkHandler + .launch(requireContext(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String?, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + .subscribe() + .disposeOnDestroyView() } override fun onReadReceiptsClicked(readReceipts: List ) { @@ -1197,9 +1204,8 @@ class RoomDetailFragment @Inject constructor( glideRequests, avatarRenderer, requireContext(), - userId, - displayName, - roomMember?.avatarUrl) + MatrixItem.UserItem(userId, displayName, roomMember?.avatarUrl) + ) .also { it.bind(composerLayout.composerEditText) }, 0, displayName.length, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index a0be8fc9dc..b2ad29668e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -58,7 +58,7 @@ data class RoomDetailViewState( val isEncrypted: Boolean = false, val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, - val syncState: SyncState = SyncState.IDLE, + val syncState: SyncState = SyncState.Idle, val highlightedEventId: String? = null, val unreadState: UnreadState = UnreadState.Unknown, val canShowJumpToReadMarker: Boolean = true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt index 2b7d64a80e..6bc93f28dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt @@ -22,6 +22,7 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.features.home.AvatarRenderer @@ -29,15 +30,13 @@ import im.vector.riotx.features.home.AvatarRenderer @EpoxyModelClass(layout = R.layout.item_display_read_receipt) abstract class DisplayReadReceiptItem : EpoxyModelWithHolder () { - @EpoxyAttribute var name: String? = null - @EpoxyAttribute var userId: String = "" - @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var timestamp: CharSequence? = null @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer override fun bind(holder: Holder) { - avatarRenderer.render(avatarUrl, userId, name, holder.avatarView) - holder.displayNameView.text = name ?: userId + avatarRenderer.render(matrixItem, holder.avatarView) + holder.displayNameView.text = matrixItem.getBestName() timestamp?.let { holder.timestampView.text = it holder.timestampView.isVisible = true diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt index 6affa582bc..3ec60217a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.riotx.features.home.room.detail.timeline.item.toMatrixItem import javax.inject.Inject /** @@ -36,9 +37,7 @@ class DisplayReadReceiptsController @Inject constructor(private val dateFormatte val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp) DisplayReadReceiptItem_() .id(it.userId) - .userId(it.userId) - .avatarUrl(it.avatarUrl) - .name(it.displayName) + .matrixItem(it.toMatrixItem()) .avatarRenderer(avatarRender) .timestamp(timestamp) .addIf(session.myUserId != it.userId, this) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index efbfd3434c..939564e780 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -44,9 +44,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid bottomSheetMessagePreviewItem { id("preview") avatarRenderer(avatarRenderer) - avatarUrl(state.informationData.avatarUrl ?: "") - senderId(state.informationData.senderId) - senderName(state.senderName()) + matrixItem(state.informationData.matrixItem) movementMethod(createLinkMovementMethod(listener)) body(body.linkify(listener)) time(state.time()) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 102412948b..1303c3aad9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -41,6 +41,7 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.html.EventHtmlRenderer +import im.vector.riotx.features.html.VectorHtmlCompressor import java.text.SimpleDateFormat import java.util.* @@ -82,6 +83,7 @@ data class MessageActionState( class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState: MessageActionState, private val eventHtmlRenderer: Lazy , + private val htmlCompressor: VectorHtmlCompressor, private val session: Session, private val noticeEventFormatter: NoticeEventFormatter, private val stringProvider: StringProvider @@ -100,6 +102,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀") + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() return fragment.messageActionViewModelFactory.create(state) @@ -167,11 +170,16 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun computeMessageBody(timelineEvent: Async ): CharSequence? { return when (timelineEvent()?.root?.getClearType()) { - EventType.MESSAGE -> { + EventType.MESSAGE, + EventType.STICKER -> { val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent() if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { - eventHtmlRenderer.get().render(messageContent.formattedBody - ?: messageContent.body) + val html = messageContent.formattedBody + ?.takeIf { it.isNotBlank() } + ?.let { htmlCompressor.compress(it) } + ?: messageContent.body + + eventHtmlRenderer.get().render(html) } else { messageContent?.body } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index c1cccbef7a..64d8950420 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -61,6 +61,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted companion object : MvRxViewModelFactory { + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: ViewEditHistoryViewState): ViewEditHistoryViewModel? { val fragment: ViewEditHistoryBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() return fragment.viewEditHistoryViewModelFactory.create(state) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 9c96f17022..9e05cdcc18 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -46,6 +46,7 @@ import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMoveme import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import im.vector.riotx.features.html.CodeVisitor import im.vector.riotx.features.html.EventHtmlRenderer +import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import me.gujun.android.span.span @@ -57,6 +58,7 @@ class MessageItemFactory @Inject constructor( private val dimensionConverter: DimensionConverter, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val htmlRenderer: Lazy , + private val htmlCompressor: VectorHtmlCompressor, private val stringProvider: StringProvider, private val imageContentRenderer: ImageContentRenderer, private val messageInformationDataFactory: MessageInformationDataFactory, @@ -179,10 +181,16 @@ class MessageItemFactory @Inject constructor( .playable(messageContent.info?.mimeType == "image/gif") .highlighted(highlight) .mediaData(data) - .clickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onImageMessageClicked(messageContent, data, view) - })) + .apply { + if (messageContent.type == MessageType.MSGTYPE_STICKER_LOCAL) { + mode(ImageContentRenderer.Mode.STICKER) + } else { + clickListener( + DebouncedClickListener(View.OnClickListener { view -> + callback?.onImageMessageClicked(messageContent, data, view) + })) + } + } } private fun buildVideoMessageItem(messageContent: MessageVideoContent, @@ -227,6 +235,7 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { val isFormatted = messageContent.formattedBody.isNullOrBlank().not() return if (isFormatted) { + // First detect if the message contains some code block(s) or inline code val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document val codeVisitor = CodeVisitor() codeVisitor.visit(localFormattedBody) @@ -240,7 +249,8 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) } CodeVisitor.Kind.NONE -> { - val formattedBody = htmlRenderer.get().render(messageContent.formattedBody!!) + val compressed = htmlCompressor.compress(messageContent.formattedBody!!) + val formattedBody = htmlRenderer.get().render(compressed) buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 784a180d00..3331fbf774 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -60,7 +60,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val avatarUrl = event.senderAvatar val memberName = event.getDisambiguatedDisplayName() val formattedMemberName = span(memberName) { - textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) + textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId)) } return MessageInformationData( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 713b60d4d8..af4c55e742 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -77,12 +77,7 @@ abstract class AbsMessageItem : BaseEventItem () { holder.timeView.visibility = View.VISIBLE holder.timeView.text = attributes.informationData.time holder.memberNameView.text = attributes.informationData.memberName - attributes.avatarRenderer.render( - attributes.informationData.avatarUrl, - attributes.informationData.senderId, - attributes.informationData.memberName?.toString(), - holder.avatarImageView - ) + attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener) holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index a2a3c9ad3b..93f7dc271d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -24,6 +24,7 @@ import androidx.core.view.children import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -54,7 +55,7 @@ abstract class MergedHeaderItem : BaseEventItem () { val data = distinctMergeData.getOrNull(index) if (data != null && view is ImageView) { view.visibility = View.VISIBLE - attributes.avatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view) + attributes.avatarRenderer.render(data.toMatrixItem(), view) } else { view.visibility = View.GONE } @@ -87,6 +88,8 @@ abstract class MergedHeaderItem : BaseEventItem () { val avatarUrl: String? ) + fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl) + data class Attributes( val isCollapsed: Boolean, val mergeData: List, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 457f30cbf4..2fd46ddf12 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -36,6 +36,8 @@ abstract class MessageImageVideoItem : AbsMessageItem = emptyList() -) : Parcelable +) : Parcelable { + + val matrixItem: MatrixItem + get() = MatrixItem.UserItem(senderId, memberName?.toString(), avatarUrl) +} @Parcelize data class ReactionInfoData( @@ -51,3 +56,5 @@ data class ReadReceiptData( val displayName: String?, val timestamp: Long ) : Parcelable + +fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index 05dedcfa22..189c358b48 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -39,13 +39,7 @@ abstract class NoticeItem : BaseEventItem () { override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = attributes.noticeText - attributes.avatarRenderer.render( - attributes.informationData.avatarUrl, - attributes.informationData.senderId, - attributes.informationData.memberName?.toString() - ?: attributes.informationData.senderId, - holder.avatarImageView - ) + attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index 9ec45b03b9..761e80dd59 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -68,6 +68,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted companion object : MvRxViewModelFactory { + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? { val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() return fragment.viewReactionsViewModelFactory.create(state) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt index 3bd097d67b..4e4e758aa2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt @@ -22,6 +22,7 @@ import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -33,10 +34,8 @@ import im.vector.riotx.features.home.AvatarRenderer abstract class RoomInvitationItem : VectorEpoxyModel () { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute lateinit var roomName: CharSequence - @EpoxyAttribute lateinit var roomId: String + @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var secondLine: CharSequence? = null - @EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var invitationAcceptInProgress: Boolean = false @EpoxyAttribute var invitationAcceptInError: Boolean = false @@ -85,9 +84,9 @@ abstract class RoomInvitationItem : VectorEpoxyModel ( rejectListener?.invoke() } } - holder.titleView.text = roomName + holder.titleView.text = matrixItem.getBestName() holder.subtitleView.setTextOrHide(secondLine) - avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) + avatarRenderer.render(matrixItem, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 00d964b28c..9e54d5fc79 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -35,7 +35,6 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.riotx.R import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.StateView @@ -61,7 +60,6 @@ data class RoomListParams( class RoomListFragment @Inject constructor( private val roomController: RoomSummaryController, val roomListViewModelFactory: RoomListViewModel.Factory, - private val errorFormatter: ErrorFormatter, private val notificationDrawerManager: NotificationDrawerManager ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt index fe208a3085..23a0fd60a2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt @@ -23,6 +23,7 @@ import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -32,11 +33,9 @@ import im.vector.riotx.features.home.AvatarRenderer abstract class RoomSummaryItem : VectorEpoxyModel () { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute lateinit var roomName: CharSequence - @EpoxyAttribute lateinit var roomId: String + @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var lastFormattedEvent: CharSequence @EpoxyAttribute lateinit var lastEventTime: CharSequence - @EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasDraft: Boolean = false @@ -48,13 +47,13 @@ abstract class RoomSummaryItem : VectorEpoxyModel () { super.bind(holder) holder.rootView.setOnClickListener(itemClickListener) holder.rootView.setOnLongClickListener(itemLongClickListener) - holder.titleView.text = roomName + holder.titleView.text = matrixItem.getBestName() holder.lastEventTimeView.text = lastEventTime holder.lastEventView.text = lastFormattedEvent holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) holder.unreadIndentIndicator.isVisible = hasUnreadMessage holder.draftView.isVisible = hasDraft - avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) + avatarRenderer.render(matrixItem, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index 85652c4139..84a5f942e8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -69,7 +70,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte return RoomInvitationItem_() .id(roomSummary.roomId) .avatarRenderer(avatarRenderer) - .roomId(roomSummary.roomId) + .matrixItem(roomSummary.toMatrixItem()) .secondLine(secondLine) .invitationAcceptInProgress(joiningRoomsIds.contains(roomSummary.roomId)) .invitationAcceptInError(joiningErrorRoomsIds.contains(roomSummary.roomId)) @@ -77,8 +78,6 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte .invitationRejectInError(rejectingErrorRoomsIds.contains(roomSummary.roomId)) .acceptListener { listener?.onAcceptRoomInvitation(roomSummary) } .rejectListener { listener?.onRejectRoomInvitation(roomSummary) } - .roomName(roomSummary.displayName) - .avatarUrl(roomSummary.avatarUrl) .listener { listener?.onRoomClicked(roomSummary) } } @@ -125,11 +124,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte return RoomSummaryItem_() .id(roomSummary.roomId) .avatarRenderer(avatarRenderer) - .roomId(roomSummary.roomId) + .matrixItem(roomSummary.toMatrixItem()) .lastEventTime(latestEventTime) .lastFormattedEvent(latestFormattedEvent) - .roomName(roomSummary.displayName) - .avatarUrl(roomSummary.avatarUrl) .showHighlighted(showHighlighted) .unreadNotificationCount(unreadCount) .hasUnreadMessage(roomSummary.hasUnreadMessages) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt index 84fd5bc6f2..8d25f5713a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.list.actions import android.view.View import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetSeparatorItem @@ -39,9 +40,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar bottomSheetRoomPreviewItem { id("preview") avatarRenderer(avatarRenderer) - roomName(roomSummary.displayName) - avatarUrl(roomSummary.avatarUrl) - roomId(roomSummary.roomId) + matrixItem(roomSummary.toMatrixItem()) settingsClickListener(View.OnClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) }) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsViewModel.kt index 7f7a1f41c4..1c4d414f18 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsViewModel.kt @@ -37,6 +37,7 @@ class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initia companion object : MvRxViewModelFactory { + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomListQuickActionsState): RoomListQuickActionsViewModel? { val fragment: RoomListQuickActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() return fragment.roomListActionsViewModelFactory.create(state) diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt index ecbf0da415..3f16666221 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -20,6 +20,7 @@ import android.content.Context import android.text.style.URLSpan import im.vector.matrix.android.api.permalinks.PermalinkData import im.vector.matrix.android.api.permalinks.PermalinkParser +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.features.home.AvatarRenderer @@ -41,8 +42,8 @@ class MxLinkTagHandler(private val glideRequests: GlideRequests, when (permalinkData) { is PermalinkData.UserLink -> { val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user?.displayName - ?: permalinkData.userId, user?.avatarUrl) + val span = PillImageSpan(glideRequests, avatarRenderer, context, MatrixItem.UserItem(permalinkData.userId, user?.displayName + ?: permalinkData.userId, user?.avatarUrl)) SpannableBuilder.setSpans( visitor.builder(), span, diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt index a192c71961..8b57006439 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt @@ -29,6 +29,7 @@ import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.google.android.material.chip.ChipDrawable import im.vector.matrix.android.api.session.room.send.UserMentionSpan +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.features.home.AvatarRenderer @@ -42,9 +43,8 @@ import java.lang.ref.WeakReference class PillImageSpan(private val glideRequests: GlideRequests, private val avatarRenderer: AvatarRenderer, private val context: Context, - override val userId: String, - override val displayName: String, - private val avatarUrl: String?) : ReplacementSpan(), UserMentionSpan { + override val matrixItem: MatrixItem +) : ReplacementSpan(), UserMentionSpan { private val pillDrawable = createChipDrawable() private val target = PillImageSpanTarget(this) @@ -53,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - avatarRenderer.render(context, glideRequests, avatarUrl, userId, displayName, target) + avatarRenderer.render(context, glideRequests, matrixItem, target) } // ReplacementSpan ***************************************************************************** @@ -101,12 +101,12 @@ class PillImageSpan(private val glideRequests: GlideRequests, private fun createChipDrawable(): ChipDrawable { val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { - text = displayName + text = matrixItem.getBestName() textEndPadding = textPadding textStartPadding = textPadding setChipMinHeightResource(R.dimen.pill_min_height) setChipIconSizeResource(R.dimen.pill_avatar_size) - chipIcon = avatarRenderer.getPlaceholderDrawable(context, userId, displayName) + chipIcon = avatarRenderer.getPlaceholderDrawable(context, matrixItem) setBounds(0, 0, intrinsicWidth, intrinsicHeight) } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/VectorHtmlCompressor.kt b/vector/src/main/java/im/vector/riotx/features/html/VectorHtmlCompressor.kt new file mode 100644 index 0000000000..9f3cf96a7e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/VectorHtmlCompressor.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 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.riotx.features.html + +import com.googlecode.htmlcompressor.compressor.Compressor +import com.googlecode.htmlcompressor.compressor.HtmlCompressor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class VectorHtmlCompressor @Inject constructor() { + + // All default options are suitable so far + private val htmlCompressor: Compressor = HtmlCompressor() + + fun compress(html: String): String { + var result = htmlCompressor.compress(html) + + // Trim space after
and, unfortunately the method setRemoveSurroundingSpaces() from the doc does not exist + result = result.replace("
", "
") + result = result.replace("
", "
") + result = result.replace("", "
") + + return result + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt index 71420448f4..b9bd9b0e1e 100644 --- a/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt +++ b/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt @@ -22,6 +22,7 @@ import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.updateLayoutParams import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.features.home.AvatarRenderer @@ -56,7 +57,7 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib fun render(sender: User, mode: Mode = Mode.LARGE) { if (mode == Mode.LARGE) { updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT } - avatarRenderer.render(sender.avatarUrl, sender.userId, sender.displayName, inviteAvatarView) + avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView) inviteIdentifierView.text = sender.userId inviteNameView.text = sender.displayName inviteLabelView.text = context.getString(R.string.send_you_invite) diff --git a/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt index 90ed466695..f1782018a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt @@ -87,7 +87,7 @@ class LinkHandlerActivity : VectorBaseActivity() { .setMessage(R.string.error_user_already_logged_in) .setCancelable(false) .setPositiveButton(R.string.logout) { _, _ -> - sessionHolder.getSafeActiveSession()?.signOut(object : MatrixCallback
{ + sessionHolder.getSafeActiveSession()?.signOut(true, object : MatrixCallback { override fun onFailure(failure: Throwable) { displayError(failure) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 6cca32cf7f..d7e37f762b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment +import io.reactivex.android.schedulers.AndroidSchedulers import javax.net.ssl.HttpsURLConnection /** @@ -60,6 +61,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { loginViewModel.viewEvents .observe() + .observeOn(AndroidSchedulers.mainThread()) .subscribe { handleLoginViewEvents(it) } @@ -78,7 +80,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { private fun showError(throwable: Throwable) { when (throwable) { is Failure.ServerError -> { - if (throwable.error.code == MatrixError.FORBIDDEN + if (throwable.error.code == MatrixError.M_FORBIDDEN && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) @@ -93,7 +95,13 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { } } - abstract fun onError(throwable: Throwable) + open fun onError(throwable: Throwable) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(throwable)) + .setPositiveButton(R.string.ok, null) + .show() + } override fun onBackPressed(toolbarButton: Boolean): Boolean { return when { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 618b3ea85d..90d6754448 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -55,4 +55,7 @@ sealed class LoginAction : VectorViewModelAction { object ResetSignMode : ResetAction() object ResetLogin : ResetAction() object ResetResetPassword : ResetAction() + + // For the soft logout case + data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 2dec402f85..d879212c3d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.Intent import android.view.View import android.view.ViewGroup +import androidx.annotation.CallSuper import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.core.view.ViewCompat @@ -43,19 +44,21 @@ import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument import im.vector.riotx.features.login.terms.toLocalizedLoginTerms +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject /** * The LoginActivity manages the fragment navigation and also display the loading View */ -class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { +open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private val loginViewModel: LoginViewModel by viewModel() private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory + @CallSuper override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -75,17 +78,17 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { // Find findViewById does not work, I do not know why // findViewById (R.id.loginLogo) ?.children - ?.first { it.id == R.id.loginLogo } + ?.firstOrNull { it.id == R.id.loginLogo } ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) } - override fun getLayoutRes() = R.layout.activity_login + final override fun getLayoutRes() = R.layout.activity_login override fun initUiAndData() { if (isFirstCreation()) { - addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) + addFirstFragment() } // Get config extra @@ -96,7 +99,8 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java) - loginSharedActionViewModel.observe() + loginSharedActionViewModel + .observe() .subscribe { handleLoginNavigation(it) } @@ -106,16 +110,20 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { .subscribe(this) { updateWithState(it) } - .disposeOnDestroy() loginViewModel.viewEvents .observe() + .observeOn(AndroidSchedulers.mainThread()) .subscribe { handleLoginViewEvents(it) } .disposeOnDestroy() } + protected open fun addFirstFragment() { + addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) + } + private fun handleLoginNavigation(loginNavigation: LoginNavigation) { // Assigning to dummy make sure we do not forget a case @Suppress("UNUSED_VARIABLE") diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt index 3ff3e902cb..e3bb539172 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginCaptchaFragment.kt @@ -29,7 +29,6 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.args import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.utils.AssetReader import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_login_captcha.* @@ -47,8 +46,7 @@ data class LoginCaptchaFragmentArgument( * In this screen, the user is asked to confirm he is not a robot */ class LoginCaptchaFragment @Inject constructor( - private val assetReader: AssetReader, - private val errorFormatter: ErrorFormatter + private val assetReader: AssetReader ) : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_captcha @@ -172,14 +170,6 @@ class LoginCaptchaFragment @Inject constructor( } } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetLogin) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 67935c1ae8..93b1b1b525 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -29,9 +29,9 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.extensions.toReducedUrl import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy @@ -45,9 +45,7 @@ import javax.inject.Inject * In signup mode: * - the user is asked for login and password */ -class LoginFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginFragment @Inject constructor() : AbstractLoginFragment() { private var passwordShown = false @@ -103,7 +101,7 @@ class LoginFragment @Inject constructor( ServerType.MatrixOrg -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(resId, state.homeServerUrlSimple) + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) loginNotice.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { @@ -114,7 +112,7 @@ class LoginFragment @Inject constructor( } ServerType.Other -> { loginServerIcon.isVisible = false - loginTitle.text = getString(resId, state.homeServerUrlSimple) + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) loginNotice.text = getString(R.string.login_server_other_text) } } @@ -134,7 +132,7 @@ class LoginFragment @Inject constructor( Observable .combineLatest( loginField.textChanges().map { it.trim().isNotEmpty() }, - passwordField.textChanges().map { it.trim().isNotEmpty() }, + passwordField.textChanges().map { it.isNotEmpty() }, BiFunction { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty } @@ -198,7 +196,7 @@ class LoginFragment @Inject constructor( is Fail -> { val error = state.asyncLoginAction.error if (error is Failure.ServerError - && error.error.code == MatrixError.FORBIDDEN + && error.error.code == MatrixError.M_FORBIDDEN && error.error.message.isEmpty()) { // Login with email, but email unknown loginFieldTil.error = getString(R.string.login_login_with_email_error) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 527b0c6802..64fb01fa5f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -31,7 +31,6 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.auth.registration.RegisterThreePid import im.vector.matrix.android.api.failure.Failure import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.is401 import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.isEmail @@ -56,7 +55,7 @@ data class LoginGenericTextInputFormFragmentArgument( /** * In this screen, the user is asked for a text input */ -class LoginGenericTextInputFormFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() { +class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFragment() { private val params: LoginGenericTextInputFormFragmentArgument by args() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index 18fcd8938b..d3a86ef769 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -25,10 +25,10 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.isEmail import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.extensions.toReducedUrl import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy @@ -38,9 +38,7 @@ import javax.inject.Inject /** * In this screen, the user is asked for email and new password to reset his password */ -class LoginResetPasswordFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment() { private var passwordShown = false @@ -57,7 +55,7 @@ class LoginResetPasswordFragment @Inject constructor( } private fun setupUi(state: LoginViewState) { - resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlSimple) + resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrl.toReducedUrl()) } private fun setupSubmitButton() { @@ -138,14 +136,6 @@ class LoginResetPasswordFragment @Inject constructor( loginViewModel.handle(LoginAction.ResetResetPassword) } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun updateWithState(state: LoginViewState) { setupUi(state) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt index 03053a9718..e7ddc78853 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt @@ -21,7 +21,6 @@ import butterknife.OnClick import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Success import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.is401 import kotlinx.android.synthetic.main.fragment_login_reset_password_mail_confirmation.* import javax.inject.Inject @@ -29,9 +28,7 @@ import javax.inject.Inject /** * In this screen, the user is asked to check his email and to click on a button once it's done */ -class LoginResetPasswordMailConfirmationFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginResetPasswordMailConfirmationFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_reset_password_mail_confirmation @@ -44,14 +41,6 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor( loginViewModel.handle(LoginAction.ResetPasswordMailConfirmed) } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetResetPassword) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt index 92d75b3998..4faeef1269 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt @@ -16,18 +16,14 @@ package im.vector.riotx.features.login -import androidx.appcompat.app.AlertDialog import butterknife.OnClick import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import javax.inject.Inject /** - * In this screen, the user is asked for email and new password to reset his password + * In this screen, we confirm to the user that his password has been reset */ -class LoginResetPasswordSuccessFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_reset_password_success @@ -36,14 +32,6 @@ class LoginResetPasswordSuccessFragment @Inject constructor( loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone) } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetResetPassword) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 6e427d0bdb..9050ea2688 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -18,11 +18,9 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import butterknife.OnClick import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.utils.openUrlInExternalBrowser import kotlinx.android.synthetic.main.fragment_login_server_selection.* import me.gujun.android.span.span @@ -31,9 +29,7 @@ import javax.inject.Inject /** * In this screen, the user will choose between matrix.org, modular or other type of homeserver */ -class LoginServerSelectionFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_server_selection @@ -107,14 +103,6 @@ class LoginServerSelectionFragment @Inject constructor( loginViewModel.handle(LoginAction.ResetHomeServerType) } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun updateWithState(state: LoginViewState) { updateSelectedChoice(state) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index d632ffe100..898ee97656 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -24,7 +24,6 @@ import androidx.core.view.isVisible import butterknife.OnClick import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.utils.openUrlInExternalBrowser import kotlinx.android.synthetic.main.fragment_login_server_url_form.* @@ -33,9 +32,7 @@ import javax.inject.Inject /** * In this screen, the user is prompted to enter a homeserver url */ -class LoginServerUrlFormFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_server_url_form diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 0484357ae2..9f084299b7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -16,20 +16,17 @@ package im.vector.riotx.features.login -import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import butterknife.OnClick import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.toReducedUrl import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import javax.inject.Inject /** * In this screen, the user is asked to sign up or to sign in to the homeserver */ -class LoginSignUpSignInSelectionFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection @@ -40,19 +37,19 @@ class LoginSignUpSignInSelectionFragment @Inject constructor( ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) loginSignupSigninServerIcon.isVisible = true - loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlSimple) + loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl()) loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) } ServerType.Modular -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular) loginSignupSigninServerIcon.isVisible = true loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular) - loginSignupSigninText.text = state.homeServerUrlSimple + loginSignupSigninText.text = state.homeServerUrl.toReducedUrl() } ServerType.Other -> { loginSignupSigninServerIcon.isVisible = false loginSignupSigninTitle.text = getString(R.string.login_server_other_title) - loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrlSimple) + loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl()) } } } @@ -84,14 +81,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor( loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetSignMode) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index ef17bea920..53de8c2c43 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -16,18 +16,14 @@ package im.vector.riotx.features.login -import androidx.appcompat.app.AlertDialog import butterknife.OnClick import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import javax.inject.Inject /** * In this screen, the user is viewing an introduction to what he can do with this application */ -class LoginSplashFragment @Inject constructor( - private val errorFormatter: ErrorFormatter -) : AbstractLoginFragment() { +class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_splash @@ -36,14 +32,6 @@ class LoginSplashFragment @Inject constructor( loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun resetViewModel() { // Nothing to do } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 00207cbfbf..baa4160351 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.login +import androidx.fragment.app.FragmentActivity import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -37,6 +38,7 @@ import im.vector.riotx.core.utils.DataSource import im.vector.riotx.core.utils.PublishDataSource import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener +import im.vector.riotx.features.signout.soft.SoftLogoutActivity import timber.log.Timber import java.util.concurrent.CancellationException @@ -60,8 +62,11 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi @JvmStatic override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? { - val activity: LoginActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.loginViewModelFactory.create(state) + return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) { + is LoginActivity -> activity.loginViewModelFactory.create(state) + is SoftLogoutActivity -> activity.loginViewModelFactory.create(state) + else -> error("Invalid Activity") + } } } @@ -97,6 +102,18 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) + is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) + } + } + + private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { + setState { + copy( + signMode = SignMode.SignIn, + loginMode = LoginMode.Sso, + homeServerUrl = action.homeServerUrl, + deviceId = action.deviceId + ) } } @@ -452,7 +469,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } private fun startAuthenticationFlow() { - // No op + // Ensure Wizard is ready loginWizard } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index e4b3fe214a..2887dd04f0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -34,6 +34,9 @@ data class LoginViewState( val resetPasswordEmail: String? = null, @PersistState val homeServerUrl: String? = null, + // For SSO session recovery + @PersistState + val deviceId: String? = null, // Network result @PersistState @@ -49,17 +52,11 @@ data class LoginViewState( || asyncResetPassword is Loading || asyncResetMailConfirmed is Loading || asyncRegistration is Loading + // Keep loading when it is success because of the delay to switch to the next Activity + || asyncLoginAction is Success } fun isUserLogged(): Boolean { return asyncLoginAction is Success } - - /** - * Ex: "https://matrix.org/" -> "matrix.org" - */ - val homeServerUrlSimple: String - get() = (homeServerUrl ?: "") - .substringAfter("://") - .trim { it == '/' } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt index 2436b1d2d1..8a12c67106 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWaitForEmailFragment.kt @@ -19,10 +19,8 @@ package im.vector.riotx.features.login import android.os.Bundle import android.os.Parcelable import android.view.View -import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.args import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.is401 import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_login_wait_for_email.* @@ -36,7 +34,7 @@ data class LoginWaitForEmailFragmentArgument( /** * In this screen, the user is asked to check his emails */ -class LoginWaitForEmailFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() { +class LoginWaitForEmailFragment @Inject constructor() : AbstractLoginFragment() { private val params: LoginWaitForEmailFragmentArgument by args() @@ -69,11 +67,7 @@ class LoginWaitForEmailFragment @Inject constructor(private val errorFormatter: // Try again, with a delay loginViewModel.handle(LoginAction.CheckIfEmailHasBeenValidated(10_000)) } else { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() + super.onError(throwable) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index eac4511b57..47388653da 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -30,10 +30,13 @@ import android.webkit.SslErrorHandler import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.activityViewModel +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.utils.AssetReader +import im.vector.riotx.features.signout.soft.SoftLogoutAction +import im.vector.riotx.features.signout.soft.SoftLogoutViewModel import kotlinx.android.synthetic.main.fragment_login_web.* import timber.log.Timber import java.net.URLDecoder @@ -44,13 +47,13 @@ import javax.inject.Inject * of the homeserver, as a fallback to login or to create an account */ class LoginWebFragment @Inject constructor( - private val assetReader: AssetReader, - private val errorFormatter: ErrorFormatter + private val assetReader: AssetReader ) : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_web private var isWebViewLoaded = false + private var isForSessionRecovery = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -60,6 +63,9 @@ class LoginWebFragment @Inject constructor( override fun updateWithState(state: LoginViewState) { setupTitle(state) + + isForSessionRecovery = state.deviceId?.isNotBlank() == true + if (!isWebViewLoaded) { setupWebView(state) isWebViewLoaded = true @@ -110,13 +116,22 @@ class LoginWebFragment @Inject constructor( } private fun launchWebView(state: LoginViewState) { - if (state.signMode == SignMode.SignIn) { - loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/login/") - } else { - // MODE_REGISTER - loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/register/") + val url = buildString { + append(state.homeServerUrl?.trim { it == '/' }) + if (state.signMode == SignMode.SignIn) { + append("/_matrix/static/client/login/") + state.deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + append("?device_id=$it") + } + } else { + // MODE_REGISTER + append("/_matrix/static/client/register/") + } } + loginWebWebView.loadUrl(url) + loginWebWebView.webViewClient = object : WebViewClient() { override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { @@ -212,10 +227,7 @@ class LoginWebFragment @Inject constructor( if (state.signMode == SignMode.SignIn) { try { if (action == "onLogin") { - val credentials = javascriptResponse.credentials - if (credentials != null) { - loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) - } + javascriptResponse.credentials?.let { notifyViewModel(it) } } } catch (e: Exception) { Timber.e(e, "## shouldOverrideUrlLoading() : failed") @@ -224,10 +236,7 @@ class LoginWebFragment @Inject constructor( // MODE_REGISTER // check the required parameters if (action == "onRegistered") { - val credentials = javascriptResponse.credentials - if (credentials != null) { - loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) - } + javascriptResponse.credentials?.let { notifyViewModel(it) } } } } @@ -239,16 +248,17 @@ class LoginWebFragment @Inject constructor( } } - override fun resetViewModel() { - loginViewModel.handle(LoginAction.ResetLogin) + private fun notifyViewModel(credentials: Credentials) { + if (isForSessionRecovery) { + val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() + softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials)) + } else { + loginViewModel.handle(LoginAction.WebLoginSuccess(credentials)) + } } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() + override fun resetViewModel() { + loginViewModel.handle(LoginAction.ResetLogin) } override fun onBackPressed(toolbarButton: Boolean): Boolean { diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 83d68f5f31..09746adc87 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -19,13 +19,12 @@ package im.vector.riotx.features.login.terms import android.os.Bundle import android.os.Parcelable import android.view.View -import androidx.appcompat.app.AlertDialog import butterknife.OnClick import com.airbnb.mvrx.args import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.toReducedUrl import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.LoginAction @@ -44,8 +43,7 @@ data class LoginTermsFragmentArgument( * LoginTermsFragment displays the list of policies the user has to accept */ class LoginTermsFragment @Inject constructor( - private val policyController: PolicyController, - private val errorFormatter: ErrorFormatter + private val policyController: PolicyController ) : AbstractLoginFragment(), PolicyController.PolicyControllerListener { @@ -106,16 +104,8 @@ class LoginTermsFragment @Inject constructor( loginViewModel.handle(LoginAction.AcceptTerms) } - override fun onError(throwable: Throwable) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(throwable)) - .setPositiveButton(R.string.ok, null) - .show() - } - override fun updateWithState(state: LoginViewState) { - policyController.homeServer = state.homeServerUrlSimple + policyController.homeServer = state.homeServerUrl.toReducedUrl() renderState() } diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index df638b462b..909fd5b8eb 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -31,11 +31,13 @@ import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideRequest +import im.vector.riotx.core.ui.model.Size import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.isLocalFile import kotlinx.android.parcel.Parcelize import timber.log.Timber import javax.inject.Inject +import kotlin.math.min class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val dimensionConverter: DimensionConverter) { @@ -56,17 +58,18 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: enum class Mode { FULL_SIZE, - THUMBNAIL + THUMBNAIL, + STICKER } fun render(data: Data, mode: Mode, imageView: ImageView) { - val (width, height) = processSize(data, mode) - imageView.layoutParams.height = height - imageView.layoutParams.width = width + val size = processSize(data, mode) + imageView.layoutParams.width = size.width + imageView.layoutParams.height = size.height // a11y imageView.contentDescription = data.filename - createGlideRequest(data, mode, imageView, width, height) + createGlideRequest(data, mode, imageView, size) .dontAnimate() .transform(RoundedCorners(dimensionConverter.dpToPx(8))) .thumbnail(0.3f) @@ -74,12 +77,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: } fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { - val (width, height) = processSize(data, mode) + val size = processSize(data, mode) // a11y imageView.contentDescription = data.filename - createGlideRequest(data, mode, imageView, width, height) + createGlideRequest(data, mode, imageView, size) .listener(object : RequestListener { override fun onLoadFailed(e: GlideException?, model: Any?, @@ -102,7 +105,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } - private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, width: Int, height: Int): GlideRequest { + private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest { return if (data.elementToDecrypt != null) { // Encrypted image GlideApp @@ -112,8 +115,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: // Clear image val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val resolvedUrl = when (mode) { - Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url) - Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) + Mode.FULL_SIZE, + Mode.STICKER -> contentUrlResolver.resolveFullSize(data.url) + Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE) } // Fallback to base url ?: data.url @@ -144,23 +148,32 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ) } - private fun processSize(data: Data, mode: Mode): Pair { + private fun processSize(data: Data, mode: Mode): Size { val maxImageWidth = data.maxWidth val maxImageHeight = data.maxHeight val width = data.width ?: maxImageWidth val height = data.height ?: maxImageHeight - var finalHeight = -1 var finalWidth = -1 + var finalHeight = -1 // if the image size is known // compute the expected height if (width > 0 && height > 0) { - if (mode == Mode.FULL_SIZE) { - finalHeight = height - finalWidth = width - } else { - finalHeight = Math.min(maxImageWidth * height / width, maxImageHeight) - finalWidth = finalHeight * width / height + when (mode) { + Mode.FULL_SIZE -> { + finalHeight = height + finalWidth = width + } + Mode.THUMBNAIL -> { + finalHeight = min(maxImageWidth * height / width, maxImageHeight) + finalWidth = finalHeight * width / height + } + Mode.STICKER -> { + // limit on width + val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2) + finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp) + finalHeight = finalWidth * height / width + } } } // ensure that some values are properly initialized @@ -170,6 +183,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: if (finalWidth < 0) { finalWidth = maxImageWidth } - return Pair(finalWidth, finalHeight) + return Size(finalWidth, finalHeight) } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 685fa04fef..08ff11217d 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -19,8 +19,11 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import android.content.Intent +import androidx.core.app.TaskStackBuilder import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.error.fatalError import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.toast import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity @@ -40,12 +43,49 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class DefaultNavigator @Inject constructor() : Navigator { +class DefaultNavigator @Inject constructor( + private val sessionHolder: ActiveSessionHolder +) : Navigator { + + override fun openRoom(context: Context, roomId: String, eventId: String?, buildTask: Boolean) { + if (sessionHolder.getSafeActiveSession()?.getRoom(roomId) == null) { + fatalError("Trying to open an unknown room $roomId") + return + } - override fun openRoom(context: Context, roomId: String, eventId: String?) { val args = RoomDetailArgs(roomId, eventId) val intent = RoomDetailActivity.newIntent(context, args) - context.startActivity(intent) + if (buildTask) { + val stackBuilder = TaskStackBuilder.create(context) + stackBuilder.addNextIntentWithParentStack(intent) + stackBuilder.startActivities() + } else { + context.startActivity(intent) + } + } + + override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) { + if (context is VectorBaseActivity) { + context.notImplemented("Open not joined room") + } else { + context.toast(R.string.not_implemented) + } + } + + override fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean) { + if (context is VectorBaseActivity) { + context.notImplemented("Open group detail") + } else { + context.toast(R.string.not_implemented) + } + } + + override fun openUserDetail(userId: String, context: Context, buildTask: Boolean) { + if (context is VectorBaseActivity) { + context.notImplemented("Open user detail") + } else { + context.toast(R.string.not_implemented) + } } override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) { @@ -55,14 +95,6 @@ class DefaultNavigator @Inject constructor() : Navigator { activity.finish() } - override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) { - if (context is VectorBaseActivity) { - context.notImplemented("Open not joined room") - } else { - context.toast(R.string.not_implemented) - } - } - override fun openRoomPreview(publicRoom: PublicRoom, context: Context) { val intent = RoomPreviewActivity.getIntent(context, publicRoom) context.startActivity(intent) @@ -105,14 +137,6 @@ class DefaultNavigator @Inject constructor() : Navigator { context.startActivity(KeysBackupManageActivity.intent(context)) } - override fun openGroupDetail(groupId: String, context: Context) { - Timber.v("Open group detail $groupId") - } - - override fun openUserDetail(userId: String, context: Context) { - Timber.v("Open user detail $userId") - } - override fun openRoomSettings(context: Context, roomId: String) { Timber.v("Open room settings$roomId") } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 83c4f7ce20..278c8fdba0 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -23,11 +23,11 @@ import im.vector.riotx.features.share.SharedData interface Navigator { - fun openRoom(context: Context, roomId: String, eventId: String? = null) + fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false) fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) - fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) + fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false) fun openRoomPreview(publicRoom: PublicRoom, context: Context) @@ -47,9 +47,9 @@ interface Navigator { fun openKeysBackupManager(context: Context) - fun openGroupDetail(groupId: String, context: Context) + fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean = false) - fun openUserDetail(userId: String, context: Context) + fun openUserDetail(userId: String, context: Context, buildTask: Boolean = false) fun openRoomSettings(context: Context, roomId: String) } diff --git a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt new file mode 100644 index 0000000000..e46adc53fc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 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.riotx.features.permalink + +import android.content.Context +import android.net.Uri +import im.vector.matrix.android.api.permalinks.PermalinkData +import im.vector.matrix.android.api.permalinks.PermalinkParser +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.rx.rx +import im.vector.riotx.features.navigation.Navigator +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PermalinkHandler @Inject constructor(private val session: Session, + private val navigator: Navigator) { + + fun launch( + context: Context, + deepLink: String?, + navigateToRoomInterceptor: NavigateToRoomInterceptor? = null, + buildTask: Boolean = false + ): Single { + val uri = deepLink?.let { Uri.parse(it) } + return launch(context, uri, navigateToRoomInterceptor, buildTask) + } + + fun launch( + context: Context, + deepLink: Uri?, + navigateToRoomInterceptor: NavigateToRoomInterceptor? = null, + buildTask: Boolean = false + ): Single { + if (deepLink == null) { + return Single.just(false) + } + return when (val permalinkData = PermalinkParser.parse(deepLink)) { + is PermalinkData.RoomLink -> { + permalinkData.getRoomId() + .observeOn(AndroidSchedulers.mainThread()) + .map { + val roomId = it.getOrNull() + if (navigateToRoomInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) { + openRoom(context, roomId, permalinkData.eventId, buildTask) + } + true + } + } + is PermalinkData.GroupLink -> { + navigator.openGroupDetail(permalinkData.groupId, context, buildTask) + Single.just(true) + } + is PermalinkData.UserLink -> { + navigator.openUserDetail(permalinkData.userId, context, buildTask) + Single.just(true) + } + is PermalinkData.FallbackLink -> { + Single.just(false) + } + } + } + + private fun PermalinkData.RoomLink.getRoomId(): Single > { + return if (isRoomAlias) { + // At the moment we are not fetching on the server as we don't handle not join room + session.rx().getRoomIdByAlias(roomIdOrAlias, false).subscribeOn(Schedulers.io()) + } else { + Single.just(Optional.from(roomIdOrAlias)) + } + } + + /** + * Open room either joined, or not + */ + private fun openRoom(context: Context, roomId: String?, eventId: String?, buildTask: Boolean) { + return if (roomId != null && session.getRoom(roomId) != null) { + navigator.openRoom(context, roomId, eventId, buildTask) + } else { + navigator.openNotJoinedRoom(context, roomId, eventId, buildTask) + } + } +} + +interface NavigateToRoomInterceptor { + + /** + * Return true if the navigation has been intercepted + */ + fun navToRoom(roomId: String?, eventId: String? = null): Boolean +} diff --git a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandlerActivity.kt b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandlerActivity.kt new file mode 100644 index 0000000000..5339a2c6f9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandlerActivity.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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.riotx.features.permalink + +import android.content.Intent +import android.os.Bundle +import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.replaceFragment +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.home.LoadingFragment +import im.vector.riotx.features.login.LoginActivity +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class PermalinkHandlerActivity : VectorBaseActivity() { + + @Inject lateinit var permalinkHandler: PermalinkHandler + @Inject lateinit var sessionHolder: ActiveSessionHolder + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_simple) + if (isFirstCreation()) { + replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java) + } + // 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() + } + + private fun startLoginActivity() { + val intent = LoginActivity.newIntent(this, null) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + finish() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt index 187566f660..5c1428cb54 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReportActivity.kt @@ -77,8 +77,7 @@ class BugReportActivity : VectorBaseActivity() { override fun onPrepareOptionsMenu(menu: Menu): Boolean { menu.findItem(R.id.ic_action_send_bug_report)?.let { - val isValid = bug_report_edit_text.text.toString().trim().length > 10 - && !bug_report_mask_view.isVisible + val isValid = !bug_report_mask_view.isVisible it.isEnabled = isValid it.icon.alpha = if (isValid) 255 else 100 @@ -90,7 +89,11 @@ class BugReportActivity : VectorBaseActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.ic_action_send_bug_report -> { - sendBugReport() + if (bug_report_edit_text.text.toString().trim().length >= 10) { + sendBugReport() + } else { + bug_report_text_input_layout.error = getString(R.string.bug_report_error_too_short) + } return true } } @@ -150,7 +153,7 @@ class BugReportActivity : VectorBaseActivity() { val myProgress = progress.coerceIn(0, 100) bug_report_progress_view.progress = myProgress - bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, "$myProgress") + bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, myProgress.toString()) } override fun onUploadSucceed() { @@ -179,7 +182,7 @@ class BugReportActivity : VectorBaseActivity() { @OnTextChanged(R.id.bug_report_edit_text) internal fun textChanged() { - invalidateOptionsMenu() + bug_report_text_input_layout.error = null } @OnCheckedChanged(R.id.bug_report_button_include_screenshot) diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt index b96542a8ce..dc353363d5 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt @@ -33,6 +33,7 @@ import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.toOnOff import im.vector.riotx.core.utils.getDeviceLocale import im.vector.riotx.features.settings.VectorLocale +import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.version.VersionProvider import okhttp3.Call @@ -44,12 +45,15 @@ import okhttp3.Response import org.json.JSONException import org.json.JSONObject import timber.log.Timber -import java.io.* +import java.io.File +import java.io.IOException +import java.io.OutputStreamWriter import java.net.HttpURLConnection -import java.util.Locale +import java.util.* import java.util.zip.GZIPOutputStream import javax.inject.Inject import javax.inject.Singleton +import kotlin.collections.ArrayList /** * BugReporter creates and sends the bug reports. @@ -57,6 +61,7 @@ import javax.inject.Singleton @Singleton class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val versionProvider: VersionProvider, + private val vectorPreferences: VectorPreferences, private val vectorFileLogger: VectorFileLogger) { var inMultiWindowMode = false @@ -230,7 +235,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) .addFormDataPart("olm_version", olmVersion) .addFormDataPart("device", Build.MODEL.trim()) - .addFormDataPart("lazy_loading", true.toOnOff()) + .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt index 95053790c8..6049db6180 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt @@ -24,9 +24,7 @@ import java.io.File import java.io.PrintWriter import java.io.StringWriter import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone +import java.util.* import java.util.logging.* import java.util.logging.Formatter import javax.inject.Inject @@ -83,7 +81,8 @@ class VectorFileLogger @Inject constructor(val context: Context, private val vec return if (vectorPreferences.labAllowedExtendedLogging()) { false } else { - priority < Log.ERROR + // Exclude debug and verbose logs + priority <= Log.DEBUG } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt index 057c5d8159..01debac5ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -42,6 +42,7 @@ class EmojiSearchResultViewModel @AssistedInject constructor( companion object : MvRxViewModelFactory { + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? { val activity: EmojiReactionPickerActivity = (viewModelContext as ActivityViewModelContext).activity() return activity.emojiSearchResultViewModelFactory.create(state) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomItem.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomItem.kt index 5e5c4fc5f1..108627e3f8 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomItem.kt @@ -21,6 +21,7 @@ import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -35,13 +36,7 @@ abstract class PublicRoomItem : VectorEpoxyModel () { lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute - var avatarUrl: String? = null - - @EpoxyAttribute - var roomId: String? = null - - @EpoxyAttribute - var roomName: String? = null + lateinit var matrixItem: MatrixItem @EpoxyAttribute var roomAlias: String? = null @@ -64,8 +59,8 @@ abstract class PublicRoomItem : VectorEpoxyModel () { override fun bind(holder: Holder) { holder.rootView.setOnClickListener { globalListener?.invoke() } - avatarRenderer.render(avatarUrl, roomId!!, roomName, holder.avatarView) - holder.nameView.text = roomName + avatarRenderer.render(matrixItem, holder.avatarView) + holder.nameView.text = matrixItem.displayName holder.aliasView.setTextOrHide(roomAlias) holder.topicView.setTextOrHide(roomTopic) // TODO Use formatter for big numbers? diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt index 183256a53e..83a1768843 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem @@ -83,9 +84,7 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri publicRoomItem { avatarRenderer(avatarRenderer) id(publicRoom.roomId) - roomId(publicRoom.roomId) - avatarUrl(publicRoom.avatarUrl) - roomName(publicRoom.name) + matrixItem(publicRoom.toMatrixItem()) roomAlias(publicRoom.canonicalAlias) roomTopic(publicRoom.topic) nbOfMembers(publicRoom.numJoinedMembers) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt index 1d8ed48b08..1e625cff75 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt @@ -26,7 +26,6 @@ import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.observeEvent @@ -42,8 +41,7 @@ import javax.inject.Inject * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect */ class PublicRoomsFragment @Inject constructor( - private val publicRoomsController: PublicRoomsController, - private val errorFormatter: ErrorFormatter + private val publicRoomsController: PublicRoomsController ) : VectorBaseFragment(), PublicRoomsController.Callback { private val viewModel: RoomDirectoryViewModel by activityViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index d89f0e2b99..dcd64c6a46 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -178,7 +178,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: copy( asyncPublicRoomsRequest = Success(data.chunk!!), // It's ok to append at the end of the list, so I use publicRooms.size() - publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size), + publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size) + // Rageshake #8206 tells that we can have several times the same room + .distinctBy { it.roomId }, hasMore = since != null ) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 1bd138552e..0fdb504c23 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Parcelable import androidx.appcompat.widget.Toolbar import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.platform.ToolbarConfigurable @@ -34,7 +35,10 @@ data class RoomPreviewData( val topic: String?, val worldReadable: Boolean, val avatarUrl: String? -) : Parcelable +) : Parcelable { + val matrixItem: MatrixItem + get() = MatrixItem.RoomItem(roomId, roomName, avatarUrl) +} class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 9003421dc7..8999b88aba 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -24,7 +24,6 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.platform.ButtonStateView import im.vector.riotx.core.platform.VectorBaseFragment @@ -37,7 +36,6 @@ import javax.inject.Inject * Note: this Fragment is also used for world readable room for the moment */ class RoomPreviewNoPreviewFragment @Inject constructor( - private val errorFormatter: ErrorFormatter, val roomPreviewViewModelFactory: RoomPreviewViewModel.Factory, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment() { @@ -51,11 +49,11 @@ class RoomPreviewNoPreviewFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupToolbar(roomPreviewNoPreviewToolbar) // Toolbar - avatarRenderer.render(roomPreviewData.avatarUrl, roomPreviewData.roomId, roomPreviewData.roomName, roomPreviewNoPreviewToolbarAvatar) + avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar) roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName // Screen - avatarRenderer.render(roomPreviewData.avatarUrl, roomPreviewData.roomId, roomPreviewData.roomName, roomPreviewNoPreviewAvatar) + avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar) roomPreviewNoPreviewName.text = roomPreviewData.roomName roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic) diff --git a/vector/src/main/java/im/vector/riotx/features/session/SessionListener.kt b/vector/src/main/java/im/vector/riotx/features/session/SessionListener.kt index 46f8fe5e64..4aef387d7c 100644 --- a/vector/src/main/java/im/vector/riotx/features/session/SessionListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/session/SessionListener.kt @@ -18,27 +18,21 @@ package im.vector.riotx.features.session import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import im.vector.matrix.android.api.failure.ConsentNotGivenError +import im.vector.matrix.android.api.failure.GlobalError import im.vector.matrix.android.api.session.Session import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.utils.LiveEvent -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class SessionListener @Inject constructor() : Session.Listener { - private val _consentNotGivenLiveData = MutableLiveData >() - val consentNotGivenLiveData: LiveData > - get() = _consentNotGivenLiveData + private val _globalErrorLiveData = MutableLiveData >() + val globalErrorLiveData: LiveData > + get() = _globalErrorLiveData - override fun onInvalidToken() { - // TODO Handle this error - Timber.e("Token is not valid anymore: handle this properly") - } - - override fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) { - _consentNotGivenLiveData.postLiveEvent(consentNotGivenError) + override fun onGlobalError(globalError: GlobalError) { + _globalErrorLiveData.postLiveEvent(globalError) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt index 666f1610b0..e32cc98123 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt @@ -65,7 +65,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree override fun onResume() { super.onResume() - Timber.v("onResume Fragment ${this.javaClass.simpleName}") + Timber.i("onResume Fragment ${this.javaClass.simpleName}") vectorActivity.supportActionBar?.setTitle(titleRes) // find the view from parent activity mLoadingView = vectorActivity.findViewById(R.id.vector_settings_spinner_views) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index ca994db62c..17f440c3dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -43,6 +43,7 @@ import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.core.utils.* import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.workers.signout.SignOutUiWorker import kotlinx.coroutines.Dispatchers @@ -176,7 +177,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { displayLoadingView() - MainActivity.restartApp(activity!!, clearCache = true, clearCredentials = false) + MainActivity.restartApp(activity!!, MainActivityArgs(clearCache = true)) false } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersController.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersController.kt index 120781874d..5f4158b542 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersController.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.settings.ignored import com.airbnb.epoxy.EpoxyController import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.resources.StringProvider @@ -44,19 +45,19 @@ class IgnoredUsersController @Inject constructor(private val stringProvider: Str buildIgnoredUserModels(nonNullViewState.ignoredUsers) } - private fun buildIgnoredUserModels(userIds: List ) { - if (userIds.isEmpty()) { + private fun buildIgnoredUserModels(users: List ) { + if (users.isEmpty()) { noResultItem { id("empty") text(stringProvider.getString(R.string.no_ignored_users)) } } else { - userIds.forEach { userId -> + users.forEach { user -> userItem { - id(userId.userId) + id(user.userId) avatarRenderer(avatarRenderer) - user(userId) - itemClickAction { callback?.onUserIdClicked(userId.userId) } + matrixItem(user.toMatrixItem()) + itemClickAction { callback?.onUserIdClicked(user.userId) } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/UserItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/UserItem.kt index a9c1b98915..23fb03d59a 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/ignored/UserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/UserItem.kt @@ -20,7 +20,7 @@ import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -37,7 +37,7 @@ abstract class UserItem : VectorEpoxyModel () { lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute - lateinit var user: User + lateinit var matrixItem: MatrixItem @EpoxyAttribute var itemClickAction: (() -> Unit)? = null @@ -45,9 +45,9 @@ abstract class UserItem : VectorEpoxyModel () { override fun bind(holder: Holder) { holder.root.setOnClickListener { itemClickAction?.invoke() } - avatarRenderer.render(user, holder.avatarImage) - holder.userIdText.setTextOrHide(user.userId) - holder.displayNameText.setTextOrHide(user.displayName) + avatarRenderer.render(matrixItem, holder.avatarImage) + holder.userIdText.setTextOrHide(matrixItem.id) + holder.displayNameText.setTextOrHide(matrixItem.displayName) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt index a6b8a5414f..6435f43d87 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt @@ -25,7 +25,6 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.observeEvent @@ -37,8 +36,7 @@ import javax.inject.Inject class VectorSettingsIgnoredUsersFragment @Inject constructor( val ignoredUsersViewModelFactory: IgnoredUsersViewModel.Factory, - private val ignoredUsersController: IgnoredUsersController, - private val errorFormatter: ErrorFormatter + private val ignoredUsersController: IgnoredUsersController ) : VectorBaseFragment(), IgnoredUsersController.Callback { override fun getLayoutResId() = R.layout.fragment_generic_recycler diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt index dd773f4c22..db4586dff5 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt @@ -40,6 +40,7 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: companion object : MvRxViewModelFactory { + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: PushGatewayViewState): PushGatewaysViewModel? { val fragment: PushGatewaysFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.pushGatewaysViewModelFactory.create(state) diff --git a/vector/src/main/java/im/vector/riotx/features/signout/hard/SignedOutActivity.kt b/vector/src/main/java/im/vector/riotx/features/signout/hard/SignedOutActivity.kt new file mode 100644 index 0000000000..f3d81c8010 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/hard/SignedOutActivity.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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.riotx.features.signout.hard + +import android.content.Context +import android.content.Intent +import butterknife.OnClick +import im.vector.matrix.android.api.failure.GlobalError +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs +import timber.log.Timber + +/** + * In this screen, the user is viewing a message informing that he has been logged out + */ +class SignedOutActivity : VectorBaseActivity() { + + override fun getLayoutRes() = R.layout.activity_signed_out + + @OnClick(R.id.signedOutSubmit) + fun submit() { + // All is already cleared when we are here + MainActivity.restartApp(this, MainActivityArgs()) + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, SignedOutActivity::class.java) + } + } + + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + // No op here + Timber.w("Ignoring invalid token global error") + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutAction.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutAction.kt new file mode 100644 index 0000000000..5916c59c55 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutAction.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class SoftLogoutAction : VectorViewModelAction { + // In case of failure to get the login flow + object RetryLoginFlow : SoftLogoutAction() + + // For password entering management + data class PasswordChanged(val password: String) : SoftLogoutAction() + object TogglePassword : SoftLogoutAction() + data class SignInAgain(val password: String) : SoftLogoutAction() + + // For signing again with SSO + data class WebLoginSuccess(val credentials: Credentials) : SoftLogoutAction() + + // To clear the current session + object ClearData : SoftLogoutAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutActivity.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutActivity.kt new file mode 100644 index 0000000000..8d61fb00b5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutActivity.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.viewModel +import im.vector.matrix.android.api.failure.GlobalError +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.replaceFragment +import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs +import im.vector.riotx.features.login.LoginActivity +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.activity_login.* +import timber.log.Timber +import javax.inject.Inject + +/** + * In this screen, the user is viewing a message informing that he has been logged out + * Extends LoginActivity to get the login with SSO and forget password functionality for (nearly) free + */ +class SoftLogoutActivity : LoginActivity() { + + private val softLogoutViewModel: SoftLogoutViewModel by viewModel() + + @Inject lateinit var softLogoutViewModelFactory: SoftLogoutViewModel.Factory + @Inject lateinit var session: Session + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun initUiAndData() { + super.initUiAndData() + + softLogoutViewModel + .subscribe(this) { + updateWithState(it) + } + + softLogoutViewModel.viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + handleSoftLogoutViewEvents(it) + } + .disposeOnDestroy() + } + + private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) { + when (softLogoutViewEvents) { + is SoftLogoutViewEvents.Error -> + showError(errorFormatter.toHumanReadable(softLogoutViewEvents.throwable)) + is SoftLogoutViewEvents.ErrorNotSameUser -> { + // Pop the backstack + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + // And inform the user + showError(getString( + R.string.soft_logout_sso_not_same_user_error, + softLogoutViewEvents.currentUserId, + softLogoutViewEvents.newUserId) + ) + } + is SoftLogoutViewEvents.ClearData -> { + MainActivity.restartApp(this, MainActivityArgs(clearCredentials = true)) + } + } + } + + private fun showError(message: String) { + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + + override fun addFirstFragment() { + replaceFragment(R.id.loginFragmentContainer, SoftLogoutFragment::class.java) + } + + private fun updateWithState(softLogoutViewState: SoftLogoutViewState) { + if (softLogoutViewState.asyncLoginAction is Success) { + MainActivity.restartApp(this, MainActivityArgs()) + } + + loginLoading.isVisible = softLogoutViewState.isLoading() + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, SoftLogoutActivity::class.java) + } + } + + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + // No op here + Timber.w("Ignoring invalid token global error") + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt new file mode 100644 index 0000000000..4f686a4a76 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutController.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft + +import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.toReducedUrl +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.login.LoginMode +import im.vector.riotx.features.signout.soft.epoxy.* +import javax.inject.Inject + +class SoftLogoutController @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: SoftLogoutViewState? = null + + init { + // We are requesting a model build directly as the first build of epoxy is on the main thread. + // It avoids to build the whole list of breadcrumbs on the main thread. + requestModelBuild() + } + + fun update(viewState: SoftLogoutViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val safeViewState = viewState ?: return + + buildHeader(safeViewState) + buildForm(safeViewState) + buildClearDataSection() + } + + private fun buildHeader(state: SoftLogoutViewState) { + loginHeaderItem { + id("header") + } + loginTitleItem { + id("title") + text(stringProvider.getString(R.string.soft_logout_title)) + } + loginTitleSmallItem { + id("signTitle") + text(stringProvider.getString(R.string.soft_logout_signin_title)) + } + loginTextItem { + id("signText1") + text(stringProvider.getString(R.string.soft_logout_signin_notice, + state.homeServerUrl.toReducedUrl(), + state.userDisplayName, + state.userId)) + } + if (state.hasUnsavedKeys) { + loginTextItem { + id("signText2") + text(stringProvider.getString(R.string.soft_logout_signin_e2e_warning_notice)) + } + } + } + + private fun buildForm(state: SoftLogoutViewState) { + when (state.asyncHomeServerLoginFlowRequest) { + is Incomplete -> { + loadingItem { + id("loading") + } + } + is Fail -> { + loginErrorWithRetryItem { + id("errorRetry") + text(errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error)) + listener { listener?.retry() } + } + } + is Success -> { + when (state.asyncHomeServerLoginFlowRequest.invoke()) { + LoginMode.Password -> { + loginPasswordFormItem { + id("passwordForm") + stringProvider(stringProvider) + passwordShown(state.passwordShown) + submitEnabled(state.submitEnabled) + onPasswordEdited { listener?.passwordEdited(it) } + errorText((state.asyncLoginAction as? Fail)?.error?.let { errorFormatter.toHumanReadable(it) }) + passwordRevealClickListener { listener?.revealPasswordClicked() } + forgetPasswordClickListener { listener?.forgetPasswordClicked() } + submitClickListener { password -> listener?.signinSubmit(password) } + } + } + LoginMode.Sso -> { + loginCenterButtonItem { + id("sso") + text(stringProvider.getString(R.string.login_signin_sso)) + listener { listener?.signinFallbackSubmit() } + } + } + LoginMode.Unsupported -> { + loginCenterButtonItem { + id("fallback") + text(stringProvider.getString(R.string.login_signin)) + listener { listener?.signinFallbackSubmit() } + } + } + LoginMode.Unknown -> Unit // Should not happen + } + } + } + } + + private fun buildClearDataSection() { + loginTitleSmallItem { + id("clearDataTitle") + text(stringProvider.getString(R.string.soft_logout_clear_data_title)) + } + loginTextItem { + id("clearDataText") + text(stringProvider.getString(R.string.soft_logout_clear_data_notice)) + } + loginRedButtonItem { + id("clearDataSubmit") + text(stringProvider.getString(R.string.soft_logout_clear_data_submit)) + listener { listener?.clearData() } + } + } + + interface Listener { + fun retry() + fun passwordEdited(password: String) + fun signinSubmit(password: String) + fun signinFallbackSubmit() + fun clearData() + fun forgetPasswordClicked() + fun revealPasswordClicked() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt new file mode 100644 index 0000000000..d3288c5b2e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.dialogs.withColoredButton +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.features.login.AbstractLoginFragment +import im.vector.riotx.features.login.LoginAction +import im.vector.riotx.features.login.LoginMode +import im.vector.riotx.features.login.LoginNavigation +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +/** + * In this screen: + * - the user is asked to enter a password to sign in again to a homeserver. + * - or to cleanup all the data + */ +class SoftLogoutFragment @Inject constructor( + private val softLogoutController: SoftLogoutController +) : AbstractLoginFragment(), SoftLogoutController.Listener { + + private val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView() + + softLogoutViewModel.subscribe(this) { softLogoutViewState -> + softLogoutController.update(softLogoutViewState) + + when (softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) { + LoginMode.Sso, + LoginMode.Unsupported -> { + // Prepare the loginViewModel for a SSO/login fallback recovery + loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId + )) + } + else -> Unit + } + } + } + + private fun setupRecyclerView() { + recyclerView.configureWith(softLogoutController) + softLogoutController.listener = this + } + + override fun onDestroyView() { + recyclerView.cleanup() + softLogoutController.listener = null + super.onDestroyView() + } + + override fun retry() { + softLogoutViewModel.handle(SoftLogoutAction.RetryLoginFlow) + } + + override fun passwordEdited(password: String) { + softLogoutViewModel.handle(SoftLogoutAction.PasswordChanged(password)) + } + + override fun signinSubmit(password: String) { + cleanupUi() + softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password)) + } + + override fun signinFallbackSubmit() { + loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + } + + override fun clearData() { + withState(softLogoutViewModel) { state -> + cleanupUi() + + val messageResId = if (state.hasUnsavedKeys) { + R.string.soft_logout_clear_data_dialog_e2e_warning_content + } else { + R.string.soft_logout_clear_data_dialog_content + } + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.soft_logout_clear_data_dialog_title) + .setMessage(messageResId) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.soft_logout_clear_data_submit) { _, _ -> + softLogoutViewModel.handle(SoftLogoutAction.ClearData) + } + .show() + .withColoredButton(DialogInterface.BUTTON_POSITIVE) + } + } + + private fun cleanupUi() { + recyclerView.hideKeyboard() + } + + override fun forgetPasswordClicked() { + loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + } + + override fun revealPasswordClicked() { + softLogoutViewModel.handle(SoftLogoutAction.TogglePassword) + } + + override fun resetViewModel() { + // No op + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewEvents.kt new file mode 100644 index 0000000000..1e48fb2a25 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewEvents.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft + +/** + * Transient events for SoftLogout + */ +sealed class SoftLogoutViewEvents { + data class ErrorNotSameUser(val currentUserId: String, val newUserId: String) : SoftLogoutViewEvents() + data class Error(val throwable: Throwable) : SoftLogoutViewEvents() + object ClearData : SoftLogoutViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt new file mode 100644 index 0000000000..baf208636b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.auth.data.LoginFlowResult +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.extensions.hasUnsavedKeys +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.utils.DataSource +import im.vector.riotx.core.utils.PublishDataSource +import im.vector.riotx.features.login.LoginMode +import timber.log.Timber + +/** + * TODO Test push: disable the pushers? + */ +class SoftLogoutViewModel @AssistedInject constructor( + @Assisted initialState: SoftLogoutViewState, + private val session: Session, + private val activeSessionHolder: ActiveSessionHolder, + private val authenticationService: AuthenticationService +) : VectorViewModel (initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SoftLogoutViewState): SoftLogoutViewModel + } + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): SoftLogoutViewState? { + val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() + val userId = activity.session.myUserId + return SoftLogoutViewState( + homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString(), + userId = userId, + deviceId = activity.session.sessionParams.credentials.deviceId ?: "", + userDisplayName = activity.session.getUser(userId)?.displayName ?: userId, + hasUnsavedKeys = activity.session.hasUnsavedKeys() + ) + } + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: SoftLogoutViewState): SoftLogoutViewModel? { + val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.softLogoutViewModelFactory.create(state) + } + } + + private var currentTask: Cancelable? = null + + private val _viewEvents = PublishDataSource () + val viewEvents: DataSource = _viewEvents + + init { + // Get the supported login flow + getSupportedLoginFlow() + } + + private fun getSupportedLoginFlow() { + val homeServerConnectionConfig = session.sessionParams.homeServerConnectionConfig + + currentTask?.cancel() + currentTask = null + authenticationService.cancelPendingLoginOrRegistration() + + setState { + copy( + asyncHomeServerLoginFlowRequest = Loading() + ) + } + + currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncHomeServerLoginFlowRequest = Fail(failure) + ) + } + } + + override fun onSuccess(data: LoginFlowResult) { + when (data) { + is LoginFlowResult.Success -> { + val loginMode = when { + // SSO login is taken first + data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso + data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password + else -> LoginMode.Unsupported + } + + if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { + notSupported() + } else { + setState { + copy( + asyncHomeServerLoginFlowRequest = Success(loginMode) + ) + } + } + } + is LoginFlowResult.OutdatedHomeserver -> { + notSupported() + } + } + } + + private fun notSupported() { + // Should not happen since it's a re-logout + // Notify the UI + setState { + copy( + asyncHomeServerLoginFlowRequest = Fail(IllegalStateException("Should not happen")) + ) + } + } + }) + } + + override fun handle(action: SoftLogoutAction) { + when (action) { + is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow() + is SoftLogoutAction.PasswordChanged -> handlePasswordChange(action) + is SoftLogoutAction.TogglePassword -> handleTogglePassword() + is SoftLogoutAction.SignInAgain -> handleSignInAgain(action) + is SoftLogoutAction.WebLoginSuccess -> handleWebLoginSuccess(action) + is SoftLogoutAction.ClearData -> handleClearData() + } + } + + private fun handleClearData() { + // Notify the Activity + _viewEvents.post(SoftLogoutViewEvents.ClearData) + } + + private fun handlePasswordChange(action: SoftLogoutAction.PasswordChanged) { + setState { + copy( + asyncLoginAction = Uninitialized, + submitEnabled = action.password.isNotBlank() + ) + } + } + + private fun handleTogglePassword() { + withState { + setState { + copy( + passwordShown = !this.passwordShown + ) + } + } + } + + private fun handleWebLoginSuccess(action: SoftLogoutAction.WebLoginSuccess) { + // User may have been connected with SSO with another userId + // We have to check this + withState { softLogoutViewState -> + if (softLogoutViewState.userId != action.credentials.userId) { + Timber.w("User login again with SSO, but using another account") + _viewEvents.post(SoftLogoutViewEvents.ErrorNotSameUser( + softLogoutViewState.userId, + action.credentials.userId)) + } else { + setState { + copy( + asyncLoginAction = Loading() + ) + } + currentTask = session.updateCredentials(action.credentials, + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + _viewEvents.post(SoftLogoutViewEvents.Error(failure)) + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + } + + override fun onSuccess(data: Unit) { + onSessionRestored() + } + } + ) + } + } + } + + private fun handleSignInAgain(action: SoftLogoutAction.SignInAgain) { + setState { + copy( + asyncLoginAction = Loading(), + // Ensure password is hidden + passwordShown = false + ) + } + currentTask = session.signInAgain(action.password, + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + + override fun onSuccess(data: Unit) { + onSessionRestored() + } + } + ) + } + + private fun onSessionRestored() { + activeSessionHolder.setActiveSession(session) + // Start the sync + session.startSync(true) + + // TODO Configure and start ? Check that the push still works... + setState { + copy( + asyncLoginAction = Success(Unit) + ) + } + } + + override fun onCleared() { + super.onCleared() + + currentTask?.cancel() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt new file mode 100644 index 0000000000..01776d1982 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewState.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft + +import com.airbnb.mvrx.* +import im.vector.riotx.features.login.LoginMode + +data class SoftLogoutViewState( + val asyncHomeServerLoginFlowRequest: Async = Uninitialized, + val asyncLoginAction: Async = Uninitialized, + val homeServerUrl: String, + val userId: String, + val deviceId: String, + val userDisplayName: String, + val hasUnsavedKeys: Boolean, + val passwordShown: Boolean = false, + val submitEnabled: Boolean = false +) : MvRxState { + + fun isLoading(): Boolean { + return asyncLoginAction is Loading + // Keep loading when it is success because of the delay to switch to the next Activity + || asyncLoginAction is Success + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/epoxy/LoginCenterButtonItem.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/epoxy/LoginCenterButtonItem.kt new file mode 100644 index 0000000000..d73955787c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/epoxy/LoginCenterButtonItem.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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.riotx.features.signout.soft.epoxy + +import android.widget.Button +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_login_centered_button) +abstract class LoginCenterButtonItem : VectorEpoxyModel () { + + @EpoxyAttribute var text: String? = null + @EpoxyAttribute var listener: (() -> Unit)? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.button.setTextOrHide(text) + holder.button.setOnClickListener { + listener?.invoke() + } + } + + class Holder : VectorEpoxyHolder() { + val button by bind