mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-01 19:56:47 +01:00
Merge remote-tracking branch 'origin/develop' into feature/eric/new-layout-navigation
# Conflicts: # vector/src/main/java/im/vector/app/features/home/HomeActivity.kt # vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt
This commit is contained in:
commit
edb2d5d78e
3
.github/workflows/triage-labelled.yml
vendored
3
.github/workflows/triage-labelled.yml
vendored
@ -98,7 +98,8 @@ jobs:
|
||||
# Skip in forks
|
||||
if: >
|
||||
github.repository == 'vector-im/element-android' &&
|
||||
(contains(github.event.issue.labels.*.name, 'Team: Delight'))
|
||||
(contains(github.event.issue.labels.*.name, 'Team: Delight') ||
|
||||
contains(github.event.issue.labels.*.name, 'Z-AppLayout'))
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
with:
|
||||
|
1
changelog.d/6506.wip
Normal file
1
changelog.d/6506.wip
Normal file
@ -0,0 +1 @@
|
||||
[App Layout] added dialog to configure app layout
|
1
changelog.d/6787.wip
Normal file
1
changelog.d/6787.wip
Normal file
@ -0,0 +1 @@
|
||||
[App Layout] Dialpad moved from bottom navigation tab to a separate activity accessed via home screen context menu
|
1
changelog.d/6801.wip
Normal file
1
changelog.d/6801.wip
Normal file
@ -0,0 +1 @@
|
||||
Adds new chat bottom sheet as the click action of the main FAB in the new app layout
|
1
changelog.d/6827.bugfix
Normal file
1
changelog.d/6827.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fixing sign in/up for homeservers that rely on the SSO fallback url
|
1
changelog.d/6864.bugfix
Normal file
1
changelog.d/6864.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fixes server selection being unable to trust certificates
|
1
changelog.d/6884.bugfix
Normal file
1
changelog.d/6884.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Ensure SyncThread is started when the app is launched after a Push has been received.
|
1
changelog.d/6884.sdk
Normal file
1
changelog.d/6884.sdk
Normal file
@ -0,0 +1 @@
|
||||
Rename `DebugService.logDbUsageInfo` (resp. `Session.logDbUsageInfo`) to `DebugService.getDbUsageInfo` (resp. `Session.getDbUsageInfo`) and return a String instead of logging. The caller may want to log the String.
|
1
changelog.d/6891.bugfix
Normal file
1
changelog.d/6891.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fixes missing firebase notifications after logging in when UnifiedPush distributor is installed
|
@ -199,7 +199,7 @@ dependencies {
|
||||
implementation libs.apache.commonsImaging
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.54'
|
||||
|
||||
testImplementation libs.tests.junit
|
||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||
|
@ -28,7 +28,7 @@ interface DebugService {
|
||||
fun getAllRealmConfigurations(): List<RealmConfiguration>
|
||||
|
||||
/**
|
||||
* Prints out info on DB size to logcat.
|
||||
* Get info on DB size.
|
||||
*/
|
||||
fun logDbUsageInfo()
|
||||
fun getDbUsageInfo(): String
|
||||
}
|
||||
|
@ -93,6 +93,8 @@ fun Throwable.isMissingEmailVerification() = this is Failure.ServerError &&
|
||||
error.code == MatrixError.M_UNAUTHORIZED &&
|
||||
error.message == "Unable to get validated threepid"
|
||||
|
||||
fun Throwable.isUnrecognisedCertificate() = this is Failure.UnrecognizedCertificateFailure
|
||||
|
||||
/**
|
||||
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
|
||||
*/
|
||||
|
@ -323,9 +323,9 @@ interface Session {
|
||||
fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
|
||||
|
||||
/**
|
||||
* Debug API, will print out info on DB size to logcat.
|
||||
* Debug API, will return info about the DB.
|
||||
*/
|
||||
fun logDbUsageInfo()
|
||||
fun getDbUsageInfo(): String
|
||||
|
||||
/**
|
||||
* Debug API, return the list of all RealmConfiguration used by this session.
|
||||
|
@ -53,6 +53,11 @@ interface SyncService {
|
||||
*/
|
||||
fun getSyncState(): SyncState
|
||||
|
||||
/**
|
||||
* This method returns true if the sync thread is alive, i.e. started.
|
||||
*/
|
||||
fun isSyncThreadAlive(): Boolean
|
||||
|
||||
/**
|
||||
* This method allows to listen the sync state.
|
||||
* @return a [LiveData] of [SyncState].
|
||||
|
@ -38,7 +38,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
|
||||
LiveEntityObserver, RealmChangeListener<RealmResults<T>> {
|
||||
|
||||
private companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("Matrix-LIVE_ENTITY_BACKGROUND")
|
||||
}
|
||||
|
||||
protected val observerScope = CoroutineScope(SupervisorJob() + BACKGROUND_HANDLER.asCoroutineDispatcher())
|
||||
|
@ -19,16 +19,15 @@ package org.matrix.android.sdk.internal.database.tools
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import timber.log.Timber
|
||||
|
||||
internal class RealmDebugTools(
|
||||
private val realmConfiguration: RealmConfiguration
|
||||
) {
|
||||
/**
|
||||
* Log info about the DB.
|
||||
* Get info about the DB.
|
||||
*/
|
||||
fun logInfo(baseName: String) {
|
||||
buildString {
|
||||
fun getInfo(baseName: String): String {
|
||||
return buildString {
|
||||
append("\n$baseName Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
|
||||
|
||||
if (BuildConfig.LOG_PRIVATE_DATA) {
|
||||
@ -54,7 +53,6 @@ internal class RealmDebugTools(
|
||||
separator()
|
||||
}
|
||||
}
|
||||
.let { Timber.i(it) }
|
||||
}
|
||||
|
||||
private fun StringBuilder.separator() = append("\n==============================================")
|
||||
|
@ -36,9 +36,9 @@ internal class DefaultDebugService @Inject constructor(
|
||||
realmConfigurationGlobal
|
||||
}
|
||||
|
||||
override fun logDbUsageInfo() {
|
||||
RealmDebugTools(realmConfigurationAuth).logInfo("Auth")
|
||||
RealmDebugTools(realmConfigurationGlobal).logInfo("Global")
|
||||
sessionManager.getLastSession()?.logDbUsageInfo()
|
||||
override fun getDbUsageInfo() = buildString {
|
||||
append(RealmDebugTools(realmConfigurationAuth).getInfo("Auth"))
|
||||
append(RealmDebugTools(realmConfigurationGlobal).getInfo("Global"))
|
||||
append(sessionManager.getLastSession()?.getDbUsageInfo())
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ internal object MatrixModule {
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main,
|
||||
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(),
|
||||
crypto = createBackgroundHandler("Matrix-Crypto_Thread").asCoroutineDispatcher(),
|
||||
dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
)
|
||||
}
|
||||
|
@ -263,11 +263,11 @@ internal class DefaultSession @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun logDbUsageInfo() {
|
||||
RealmDebugTools(realmConfiguration).logInfo("Session")
|
||||
RealmDebugTools(realmConfigurationCrypto).logInfo("Crypto")
|
||||
RealmDebugTools(realmConfigurationIdentity).logInfo("Identity")
|
||||
RealmDebugTools(realmConfigurationContentScanner).logInfo("ContentScanner")
|
||||
override fun getDbUsageInfo() = buildString {
|
||||
append(RealmDebugTools(realmConfiguration).getInfo("Session"))
|
||||
append(RealmDebugTools(realmConfigurationCrypto).getInfo("Crypto"))
|
||||
append(RealmDebugTools(realmConfigurationIdentity).getInfo("Identity"))
|
||||
append(RealmDebugTools(realmConfigurationContentScanner).getInfo("ContentScanner"))
|
||||
}
|
||||
|
||||
override fun getRealmConfigurations(): List<RealmConfiguration> {
|
||||
|
@ -55,7 +55,7 @@ internal class EventSenderProcessorThread @Inject constructor(
|
||||
private val queuedTaskFactory: QueuedTaskFactory,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val memento: QueueMemento
|
||||
) : Thread("SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor {
|
||||
) : Thread("Matrix-SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor {
|
||||
|
||||
private fun markAsManaged(task: QueuedTask) {
|
||||
memento.track(task)
|
||||
|
@ -76,7 +76,7 @@ internal class DefaultTimeline(
|
||||
) : Timeline {
|
||||
|
||||
companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread")
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("Matrix-DefaultTimeline_Thread")
|
||||
}
|
||||
|
||||
override val timelineID = UUID.randomUUID().toString()
|
||||
|
@ -73,6 +73,8 @@ internal class DefaultSyncService @Inject constructor(
|
||||
|
||||
override fun getSyncState() = getSyncThread().currentState()
|
||||
|
||||
override fun isSyncThreadAlive() = getSyncThread().isAlive
|
||||
|
||||
override fun getSyncRequestStateFlow() = syncRequestStateTracker.syncRequestState
|
||||
|
||||
override fun hasAlreadySynced(): Boolean {
|
||||
|
@ -62,7 +62,7 @@ internal class SyncThread @Inject constructor(
|
||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||
private val activeCallHandler: ActiveCallHandler,
|
||||
private val lightweightSettingsStorage: DefaultLightweightSettingsStorage
|
||||
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||
) : Thread("Matrix-SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||
|
||||
private var state: SyncState = SyncState.Idle
|
||||
private var liveState = MutableLiveData(state)
|
||||
|
@ -49,13 +49,13 @@ internal class DefaultBackgroundDetectionObserver : BackgroundDetectionObserver
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.v("App returning to foreground…")
|
||||
Timber.d("App returning to foreground…")
|
||||
isInBackground = false
|
||||
listeners.forEach { it.onMoveToForeground() }
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
Timber.v("App going to background…")
|
||||
Timber.d("App going to background…")
|
||||
isInBackground = true
|
||||
listeners.forEach { it.onMoveToBackground() }
|
||||
}
|
||||
|
@ -413,7 +413,7 @@ dependencies {
|
||||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.54'
|
||||
|
||||
// FlowBinding
|
||||
implementation libs.github.flowBinding
|
||||
|
@ -348,6 +348,7 @@
|
||||
<activity android:name=".features.location.LocationSharingActivity" />
|
||||
<activity android:name=".features.location.live.map.LiveLocationMapViewActivity" />
|
||||
<activity android:name=".features.settings.font.FontScaleSettingActivity"/>
|
||||
<activity android:name=".features.call.dialpad.PstnDialActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
|
@ -266,7 +266,7 @@ class VectorApplication :
|
||||
}
|
||||
|
||||
private fun createFontThreadHandler(): Handler {
|
||||
val handlerThread = HandlerThread("fonts")
|
||||
val handlerThread = HandlerThread("Vector-fonts")
|
||||
handlerThread.start()
|
||||
return Handler(handlerThread.looper)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||
import arrow.core.Option
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.core.extensions.configureAndStart
|
||||
import im.vector.app.core.extensions.startSyncing
|
||||
import im.vector.app.core.pushers.UnifiedPushHelper
|
||||
import im.vector.app.core.services.GuardServiceStarter
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
@ -100,10 +101,16 @@ class ActiveSessionHolder @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun getOrInitializeSession(startSync: Boolean): Session? {
|
||||
return activeSessionReference.get() ?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session ->
|
||||
setActiveSession(session)
|
||||
session.configureAndStart(applicationContext, startSyncing = startSync)
|
||||
}
|
||||
return activeSessionReference.get()
|
||||
?.also {
|
||||
if (startSync && !it.syncService().isSyncThreadAlive()) {
|
||||
it.startSyncing(applicationContext)
|
||||
}
|
||||
}
|
||||
?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session ->
|
||||
setActiveSession(session)
|
||||
session.configureAndStart(applicationContext, startSyncing = startSync)
|
||||
}
|
||||
}
|
||||
|
||||
fun isWaitingForSessionInitialization() = activeSessionReference.get() == null && authenticationService.hasAuthenticatedSessions()
|
||||
|
@ -63,6 +63,7 @@ import im.vector.app.features.home.room.detail.TimelineFragment
|
||||
import im.vector.app.features.home.room.detail.search.SearchFragment
|
||||
import im.vector.app.features.home.room.list.RoomListFragment
|
||||
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
|
||||
import im.vector.app.features.home.room.list.home.NewChatBottomSheet
|
||||
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
|
||||
import im.vector.app.features.location.LocationSharingFragment
|
||||
import im.vector.app.features.location.preview.LocationPreviewFragment
|
||||
@ -191,6 +192,11 @@ interface FragmentModule {
|
||||
@FragmentKey(RoomListFragment::class)
|
||||
fun bindRoomListFragment(fragment: RoomListFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(NewChatBottomSheet::class)
|
||||
fun bindNewChatBottomSheetFragment(fragment: NewChatBottomSheet): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(LocalePickerFragment::class)
|
||||
|
@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.session.sync.FilterService
|
||||
import timber.log.Timber
|
||||
|
||||
fun Session.configureAndStart(context: Context, startSyncing: Boolean = true) {
|
||||
Timber.i("Configure and start session for $myUserId")
|
||||
Timber.i("Configure and start session for $myUserId. startSyncing: $startSyncing")
|
||||
open()
|
||||
filterService().setFilter(FilterService.FilterPreset.ElementFilter)
|
||||
if (startSyncing) {
|
||||
|
@ -92,8 +92,6 @@ class UnifiedPushHelper @Inject constructor(
|
||||
return@launch
|
||||
}
|
||||
|
||||
// By default, use internal solution (fcm/background sync)
|
||||
UnifiedPush.saveDistributor(context, context.packageName)
|
||||
val distributors = UnifiedPush.getDistributors(context)
|
||||
|
||||
if (distributors.size == 1 && !force) {
|
||||
@ -101,7 +99,14 @@ class UnifiedPushHelper @Inject constructor(
|
||||
UnifiedPush.registerApp(context)
|
||||
onDoneRunnable?.run()
|
||||
} else {
|
||||
openDistributorDialogInternal(activity, pushersManager, onDoneRunnable, distributors, !force, !force)
|
||||
openDistributorDialogInternal(
|
||||
activity = activity,
|
||||
pushersManager = pushersManager,
|
||||
onDoneRunnable = onDoneRunnable,
|
||||
distributors = distributors,
|
||||
unregisterFirst = force,
|
||||
cancellable = !force
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -165,6 +170,12 @@ class UnifiedPushHelper @Inject constructor(
|
||||
onDoneRunnable?.run()
|
||||
}
|
||||
}
|
||||
.setOnCancelListener {
|
||||
// By default, use internal solution (fcm/background sync)
|
||||
UnifiedPush.saveDistributor(context, context.packageName)
|
||||
UnifiedPush.registerApp(context)
|
||||
onDoneRunnable?.run()
|
||||
}
|
||||
.setCancelable(cancellable)
|
||||
.show()
|
||||
}
|
||||
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.dialpad
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.addFragment
|
||||
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||
import im.vector.app.features.settings.VectorLocale
|
||||
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PstnDialActivity : SimpleFragmentActivity() {
|
||||
|
||||
@Inject lateinit var callManager: WebRtcCallManager
|
||||
@Inject lateinit var directRoomHelper: DirectRoomHelper
|
||||
@Inject lateinit var session: Session
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
private var progress: AppCompatDialog? = null
|
||||
|
||||
override fun getTitleRes(): Int = R.string.call
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (isFirstCreation()) {
|
||||
addFragment(
|
||||
views.container,
|
||||
createDialPadFragment()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartCallWithPhoneNumber(rawNumber: String) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
showLoadingDialog()
|
||||
val result = DialPadLookup(session, callManager, directRoomHelper).lookupPhoneNumber(rawNumber)
|
||||
callManager.startOutgoingCall(result.roomId, result.userId, isVideoCall = false)
|
||||
dismissLoadingDialog()
|
||||
finish()
|
||||
} catch (failure: Throwable) {
|
||||
dismissLoadingDialog()
|
||||
displayErrorDialog(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDialPadFragment(): Fragment {
|
||||
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, DialPadFragment::class.java.name)
|
||||
return (fragment as DialPadFragment).apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
||||
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
||||
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
||||
}
|
||||
callback = object : DialPadFragment.Callback {
|
||||
override fun onOkClicked(formatted: String?, raw: String?) {
|
||||
if (raw.isNullOrEmpty()) return
|
||||
handleStartCallWithPhoneNumber(raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoadingDialog() {
|
||||
progress?.dismiss()
|
||||
progress = MaterialProgressDialog(this)
|
||||
.show(getString(R.string.please_wait))
|
||||
}
|
||||
|
||||
private fun dismissLoadingDialog() {
|
||||
progress?.dismiss()
|
||||
}
|
||||
|
||||
private fun displayErrorDialog(throwable: Throwable) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(errorFormatter.toHumanReadable(throwable))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
@ -59,6 +59,7 @@ import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.disclaimer.showDisclaimerDialog
|
||||
import im.vector.app.features.home.room.list.actions.RoomListSharedAction
|
||||
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
|
||||
import im.vector.app.features.home.room.list.home.layout.HomeLayoutSettingBottomDialogFragment
|
||||
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
||||
import im.vector.app.features.matrixto.OriginOfMatrixTo
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
@ -292,6 +293,11 @@ class HomeActivity :
|
||||
.show(supportFragmentManager, "SPACE_SETTINGS")
|
||||
}
|
||||
|
||||
private fun showLayoutSettings() {
|
||||
HomeLayoutSettingBottomDialogFragment()
|
||||
.show(supportFragmentManager, "LAYOUT_SETTINGS")
|
||||
}
|
||||
|
||||
private fun openSpaceInvite(spaceId: String) {
|
||||
SpaceInviteBottomSheet.newInstance(spaceId)
|
||||
.show(supportFragmentManager, "SPACE_INVITE")
|
||||
@ -613,6 +619,10 @@ class HomeActivity :
|
||||
navigator.openSettings(this)
|
||||
true
|
||||
}
|
||||
R.id.menu_home_layout_settings -> {
|
||||
showLayoutSettings()
|
||||
true
|
||||
}
|
||||
R.id.menu_home_invite_friends -> {
|
||||
launchInviteFriends()
|
||||
true
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.app.features.home
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@ -45,12 +46,11 @@ import im.vector.app.core.ui.views.KeysBackupBanner
|
||||
import im.vector.app.databinding.FragmentNewHomeDetailBinding
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.dialpad.DialPadFragment
|
||||
import im.vector.app.features.call.dialpad.PstnDialActivity
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
|
||||
import im.vector.app.features.popup.PopupAlertManager
|
||||
import im.vector.app.features.popup.VerificationVectorAlert
|
||||
import im.vector.app.features.settings.VectorLocale
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
@ -102,6 +102,10 @@ class NewHomeDetailFragment @Inject constructor(
|
||||
viewModel.handle(HomeDetailAction.MarkAllRoomsRead)
|
||||
true
|
||||
}
|
||||
R.id.menu_home_dialpad -> {
|
||||
startActivity(Intent(requireContext(), PstnDialActivity::class.java))
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@ -110,6 +114,7 @@ class NewHomeDetailFragment @Inject constructor(
|
||||
withState(viewModel) { state ->
|
||||
val isRoomList = state.currentTab is HomeTab.RoomList
|
||||
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = isRoomList && hasUnreadRooms
|
||||
menu.findItem(R.id.menu_home_dialpad).isVisible = state.showDialPadTab
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,14 +147,10 @@ class NewHomeDetailFragment @Inject constructor(
|
||||
updateUIForTab(currentTab)
|
||||
}
|
||||
|
||||
viewModel.onEach(HomeDetailViewState::showDialPadTab) { showDialPadTab ->
|
||||
updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab)
|
||||
}
|
||||
|
||||
viewModel.observeViewEvents { viewEvent ->
|
||||
when (viewEvent) {
|
||||
HomeDetailViewEvents.CallStarted -> handleCallStarted()
|
||||
is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure)
|
||||
HomeDetailViewEvents.CallStarted -> Unit
|
||||
is HomeDetailViewEvents.FailToCall -> Unit
|
||||
HomeDetailViewEvents.Loading -> showLoadingDialog()
|
||||
}
|
||||
}
|
||||
@ -179,6 +180,17 @@ class NewHomeDetailFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
val previousSpaceId = spaceStateHandler.getSpaceBackstack().removeLastOrNull()
|
||||
val parentSpaceId = spaceStateHandler.getCurrentSpace()?.flattenParentIds?.lastOrNull()
|
||||
setCurrentSpace(previousSpaceId ?: parentSpaceId)
|
||||
}
|
||||
|
||||
private fun setCurrentSpace(spaceId: String?) {
|
||||
spaceStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
|
||||
sharedActionViewModel.post(HomeActivitySharedAction.OnCloseSpace)
|
||||
}
|
||||
|
||||
private fun handleCallStarted() {
|
||||
dismissLoadingDialog()
|
||||
val fragmentTag = HomeTab.DialPad.toFragmentTag()
|
||||
@ -333,30 +345,15 @@ class NewHomeDetailFragment @Inject constructor(
|
||||
add(R.id.roomListContainer, HomeRoomListFragment::class.java, null, fragmentTag)
|
||||
}
|
||||
is HomeTab.DialPad -> {
|
||||
add(R.id.roomListContainer, createDialPadFragment(), fragmentTag)
|
||||
throw NotImplementedError("this tab shouldn't exists when app layout is enabled")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tab is HomeTab.DialPad) {
|
||||
(fragmentToShow as? DialPadFragment)?.applyCallback()
|
||||
}
|
||||
attach(fragmentToShow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDialPadFragment(): Fragment {
|
||||
val fragment = childFragmentManager.fragmentFactory.instantiate(vectorBaseActivity.classLoader, DialPadFragment::class.java.name)
|
||||
return (fragment as DialPadFragment).apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
|
||||
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
|
||||
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
|
||||
}
|
||||
applyCallback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTabVisibilitySafely(tabId: Int, isVisible: Boolean) {
|
||||
val wasVisible = views.bottomNavigationView.menu.findItem(tabId).isVisible
|
||||
views.bottomNavigationView.menu.findItem(tabId).isVisible = isVisible
|
||||
@ -458,9 +455,8 @@ class NewHomeDetailFragment @Inject constructor(
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean) = try {
|
||||
val lastSpace = spaceStateHandler.popSpaceBackstack()
|
||||
spaceStateHandler.setCurrentSpace(lastSpace, isForwardNavigation = false)
|
||||
override fun onBackPressed(toolbarButton: Boolean) = if (spaceStateHandler.getCurrentSpace() != null) {
|
||||
navigateBack()
|
||||
true
|
||||
} catch (e: NoSuchElementException) {
|
||||
false
|
||||
|
@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
|
||||
private const val THREAD_NAME = "Timeline_Building_Thread"
|
||||
private const val THREAD_NAME = "Vector-Timeline_Building_Thread"
|
||||
|
||||
object TimelineAsyncHelper {
|
||||
|
||||
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.list.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import javax.inject.Inject
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "layout_preferences")
|
||||
|
||||
class HomeLayoutPreferencesStore @Inject constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
|
||||
private val areRecentsEnbabled = booleanPreferencesKey("SETTINGS_PREFERENCES_HOME_RECENTS")
|
||||
private val areFiltersEnabled = booleanPreferencesKey("SETTINGS_PREFERENCES_HOME_FILTERS")
|
||||
private val isAZOrderingEnabled = booleanPreferencesKey("SETTINGS_PREFERENCES_USE_AZ_ORDER")
|
||||
|
||||
val areRecentsEnabledFlow: Flow<Boolean> = context.dataStore.data
|
||||
.map { preferences -> preferences[areRecentsEnbabled].orFalse() }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val areFiltersEnabledFlow: Flow<Boolean> = context.dataStore.data
|
||||
.map { preferences -> preferences[areFiltersEnabled].orFalse() }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val isAZOrderingEnabledFlow: Flow<Boolean> = context.dataStore.data
|
||||
.map { preferences -> preferences[isAZOrderingEnabled].orFalse() }
|
||||
.distinctUntilChanged()
|
||||
|
||||
suspend fun setRecentsEnabled(isEnabled: Boolean) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings[areRecentsEnbabled] = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setFiltersEnabled(isEnabled: Boolean) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings[areFiltersEnabled] = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setAZOrderingEnabled(isEnabled: Boolean) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings[isAZOrderingEnabled] = isEnabled
|
||||
}
|
||||
}
|
||||
}
|
@ -74,6 +74,8 @@ class HomeRoomListFragment @Inject constructor(
|
||||
|
||||
private lateinit var stateRestorer: LayoutManagerStateRestorer
|
||||
|
||||
private val newChatBottomSheet = NewChatBottomSheet()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomListBinding {
|
||||
return FragmentRoomListBinding.inflate(inflater, container, false)
|
||||
}
|
||||
@ -160,13 +162,22 @@ class HomeRoomListFragment @Inject constructor(
|
||||
}.launchIn(lifecycleScope)
|
||||
|
||||
views.roomListView.adapter = concatAdapter
|
||||
|
||||
// we need to force scroll when recents/filter tabs are added to make them visible
|
||||
concatAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == 0) {
|
||||
layoutManager.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupFabs() {
|
||||
showFABs()
|
||||
|
||||
views.newLayoutCreateChatButton.setOnClickListener {
|
||||
// Click action for create chat modal goes here (Issue #6717)
|
||||
newChatBottomSheet.show(requireActivity().supportFragmentManager, NewChatBottomSheet.TAG)
|
||||
}
|
||||
|
||||
views.newLayoutOpenSpacesButton.setOnClickListener {
|
||||
@ -203,6 +214,9 @@ class HomeRoomListFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun setUpAdapters(sections: Set<HomeRoomSection>) {
|
||||
concatAdapter.adapters.forEach {
|
||||
concatAdapter.removeAdapter(it)
|
||||
}
|
||||
sections.forEach {
|
||||
concatAdapter.addAdapter(getAdapterForData(it))
|
||||
}
|
||||
@ -232,12 +246,11 @@ class HomeRoomListFragment @Inject constructor(
|
||||
is HomeRoomSection.RoomSummaryData -> {
|
||||
HomeFilteredRoomsController(
|
||||
roomSummaryItemFactory,
|
||||
showFilters = section.showFilters,
|
||||
).also { controller ->
|
||||
controller.listener = this
|
||||
controller.onFilterChanged = ::onRoomFilterChanged
|
||||
section.filtersData.onEach {
|
||||
controller.submitFiltersData(it)
|
||||
controller.submitFiltersData(it.getOrNull())
|
||||
}.launchIn(lifecycleScope)
|
||||
section.list.observe(viewLifecycleOwner) { list ->
|
||||
controller.submitList(list)
|
||||
|
@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -53,12 +54,14 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.state.isPublic
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
|
||||
class HomeRoomListViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: HomeRoomListViewState,
|
||||
private val session: Session,
|
||||
private val spaceStateHandler: SpaceStateHandler,
|
||||
private val preferencesStore: HomeLayoutPreferencesStore,
|
||||
) : VectorViewModel<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
@ -82,17 +85,30 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
|
||||
init {
|
||||
configureSections()
|
||||
observePreferences()
|
||||
}
|
||||
|
||||
private fun configureSections() {
|
||||
private fun observePreferences() {
|
||||
preferencesStore.areRecentsEnabledFlow.onEach {
|
||||
configureSections()
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
preferencesStore.isAZOrderingEnabledFlow.onEach {
|
||||
configureSections()
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun configureSections() = viewModelScope.launch {
|
||||
val newSections = mutableSetOf<HomeRoomSection>()
|
||||
|
||||
newSections.add(getRecentRoomsSection())
|
||||
val areSettingsEnabled = preferencesStore.areRecentsEnabledFlow.first()
|
||||
|
||||
if (areSettingsEnabled) {
|
||||
newSections.add(getRecentRoomsSection())
|
||||
}
|
||||
newSections.add(getFilteredRoomsSection())
|
||||
|
||||
viewModelScope.launch {
|
||||
_sections.emit(newSections)
|
||||
}
|
||||
_sections.emit(newSections)
|
||||
|
||||
setState {
|
||||
copy(state = StateView.State.Content)
|
||||
@ -111,13 +127,17 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
|
||||
private suspend fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
|
||||
val builder = RoomSummaryQueryParams.Builder().also {
|
||||
it.memberships = listOf(Membership.JOIN)
|
||||
}
|
||||
|
||||
val params = getFilteredQueryParams(HomeRoomFilter.ALL, builder.build())
|
||||
val sortOrder = RoomSortOrder.ACTIVITY // #6506
|
||||
val sortOrder = if (preferencesStore.isAZOrderingEnabledFlow.first()) {
|
||||
RoomSortOrder.NAME
|
||||
} else {
|
||||
RoomSortOrder.ACTIVITY
|
||||
}
|
||||
|
||||
val liveResults = session.roomService().getFilteredPagedRoomSummariesLive(
|
||||
params,
|
||||
@ -135,19 +155,18 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
.onEach { selectedSpaceOption ->
|
||||
val selectedSpace = selectedSpaceOption.orNull()
|
||||
liveResults.queryParams = liveResults.queryParams.copy(
|
||||
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
|
||||
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
|
||||
)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
return HomeRoomSection.RoomSummaryData(
|
||||
list = liveResults.livePagedList,
|
||||
showFilters = true, // #6506
|
||||
filtersData = getFiltersDataFlow()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFiltersDataFlow(): SharedFlow<List<HomeRoomFilter>> {
|
||||
val flow = MutableSharedFlow<List<HomeRoomFilter>>(replay = 1)
|
||||
private fun getFiltersDataFlow(): SharedFlow<Optional<List<HomeRoomFilter>>> {
|
||||
val flow = MutableSharedFlow<Optional<List<HomeRoomFilter>>>(replay = 1)
|
||||
|
||||
val favouritesFlow = session.flow()
|
||||
.liveRoomSummaries(
|
||||
@ -168,25 +187,28 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
.map { it.isNotEmpty() }
|
||||
.distinctUntilChanged()
|
||||
|
||||
favouritesFlow.combine(dmsFLow) { hasFavourite, hasDm ->
|
||||
hasFavourite to hasDm
|
||||
}.onEach { (hasFavourite, hasDm) ->
|
||||
val filtersData = mutableListOf(
|
||||
HomeRoomFilter.ALL,
|
||||
HomeRoomFilter.UNREADS
|
||||
)
|
||||
if (hasFavourite) {
|
||||
filtersData.add(
|
||||
HomeRoomFilter.FAVOURITES
|
||||
combine(favouritesFlow, dmsFLow, preferencesStore.areFiltersEnabledFlow) { hasFavourite, hasDm, areFiltersEnabled ->
|
||||
Triple(hasFavourite, hasDm, areFiltersEnabled)
|
||||
}.onEach { (hasFavourite, hasDm, areFiltersEnabled) ->
|
||||
if (areFiltersEnabled) {
|
||||
val filtersData = mutableListOf(
|
||||
HomeRoomFilter.ALL,
|
||||
HomeRoomFilter.UNREADS
|
||||
)
|
||||
if (hasFavourite) {
|
||||
filtersData.add(
|
||||
HomeRoomFilter.FAVOURITES
|
||||
)
|
||||
}
|
||||
if (hasDm) {
|
||||
filtersData.add(
|
||||
HomeRoomFilter.PEOPlE
|
||||
)
|
||||
}
|
||||
flow.emit(Optional.from(filtersData))
|
||||
} else {
|
||||
flow.emit(Optional.empty())
|
||||
}
|
||||
if (hasDm) {
|
||||
filtersData.add(
|
||||
HomeRoomFilter.PEOPlE
|
||||
)
|
||||
}
|
||||
|
||||
flow.emit(filtersData)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
return flow
|
||||
|
@ -21,12 +21,12 @@ import androidx.paging.PagedList
|
||||
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
sealed class HomeRoomSection {
|
||||
data class RoomSummaryData(
|
||||
val list: LiveData<PagedList<RoomSummary>>,
|
||||
val showFilters: Boolean,
|
||||
val filtersData: SharedFlow<List<HomeRoomFilter>>
|
||||
val filtersData: SharedFlow<Optional<List<HomeRoomFilter>>>,
|
||||
) : HomeRoomSection()
|
||||
|
||||
data class RecentRoomsData(
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.list.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.databinding.FragmentNewChatBottomSheetBinding
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NewChatBottomSheet @Inject constructor() : BottomSheetDialogFragment() {
|
||||
|
||||
@Inject lateinit var navigator: Navigator
|
||||
|
||||
private lateinit var binding: FragmentNewChatBottomSheetBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentNewChatBottomSheetBinding.inflate(inflater, container, false)
|
||||
initFABs()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun initFABs() {
|
||||
binding.startChat.setOnClickListener {
|
||||
navigator.openCreateDirectRoom(requireActivity())
|
||||
}
|
||||
|
||||
binding.createRoom.setOnClickListener {
|
||||
navigator.openCreateRoom(requireActivity())
|
||||
}
|
||||
|
||||
binding.exploreRooms.setOnClickListener {
|
||||
navigator.openRoomDirectory(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NewChatBottomSheet"
|
||||
}
|
||||
}
|
@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
||||
class HomeFilteredRoomsController(
|
||||
private val roomSummaryItemFactory: RoomSummaryItemFactory,
|
||||
private val showFilters: Boolean,
|
||||
) : PagedListEpoxyController<RoomSummary>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
@ -48,7 +47,7 @@ class HomeFilteredRoomsController(
|
||||
|
||||
override fun addModels(models: List<EpoxyModel<*>>) {
|
||||
val host = this
|
||||
if (showFilters) {
|
||||
if (host.filtersData != null) {
|
||||
roomFilterHeaderItem {
|
||||
id("filter_header")
|
||||
filtersData(host.filtersData)
|
||||
@ -58,7 +57,7 @@ class HomeFilteredRoomsController(
|
||||
super.addModels(models)
|
||||
}
|
||||
|
||||
fun submitFiltersData(data: List<HomeRoomFilter>) {
|
||||
fun submitFiltersData(data: List<HomeRoomFilter>?) {
|
||||
this.filtersData = data
|
||||
requestForcedModelBuild()
|
||||
}
|
||||
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.list.home.layout
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetHomeLayoutSettingsBinding
|
||||
import im.vector.app.features.home.room.list.home.HomeLayoutPreferencesStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeLayoutSettingBottomDialogFragment : VectorBaseBottomSheetDialogFragment<BottomSheetHomeLayoutSettingsBinding>() {
|
||||
|
||||
@Inject lateinit var preferencesStore: HomeLayoutPreferencesStore
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetHomeLayoutSettingsBinding {
|
||||
return BottomSheetHomeLayoutSettingsBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
views.homeLayoutSettingsRecents.isChecked = preferencesStore.areRecentsEnabledFlow.first()
|
||||
views.homeLayoutSettingsFilters.isChecked = preferencesStore.areFiltersEnabledFlow.first()
|
||||
|
||||
if (preferencesStore.isAZOrderingEnabledFlow.first()) {
|
||||
views.homeLayoutSettingsSortName.isChecked = true
|
||||
} else {
|
||||
views.homeLayoutSettingsSortActivity.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
views.homeLayoutSettingsRecents.setOnCheckedChangeListener { _, isChecked ->
|
||||
setRecentsEnabled(isChecked)
|
||||
}
|
||||
views.homeLayoutSettingsFilters.setOnCheckedChangeListener { _, isChecked ->
|
||||
setFiltersEnabled(isChecked)
|
||||
}
|
||||
views.homeLayoutSettingsSortGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||
setAzOrderingEnabled(checkedId == R.id.home_layout_settings_sort_name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRecentsEnabled(isEnabled: Boolean) = lifecycleScope.launch {
|
||||
preferencesStore.setRecentsEnabled(isEnabled)
|
||||
}
|
||||
|
||||
private fun setFiltersEnabled(isEnabled: Boolean) = lifecycleScope.launch {
|
||||
preferencesStore.setFiltersEnabled(isEnabled)
|
||||
}
|
||||
|
||||
private fun setAzOrderingEnabled(isEnabled: Boolean) = lifecycleScope.launch {
|
||||
preferencesStore.setAZOrderingEnabled(isEnabled)
|
||||
}
|
||||
}
|
@ -59,7 +59,13 @@ class RecentRoomCarouselController @Inject constructor(
|
||||
data?.let { data ->
|
||||
carousel {
|
||||
id("recents_carousel")
|
||||
padding(Carousel.Padding(host.hPadding, host.itemSpacing))
|
||||
padding(Carousel.Padding(
|
||||
host.hPadding,
|
||||
0,
|
||||
host.hPadding,
|
||||
0,
|
||||
host.itemSpacing)
|
||||
)
|
||||
withModelsFrom(data) { roomSummary ->
|
||||
val onClick = host.listener?.let { it::onRoomClicked }
|
||||
val onLongClick = host.listener?.let { it::onRoomLongClicked }
|
||||
|
@ -85,7 +85,7 @@ abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragmen
|
||||
|
||||
private fun prefetchIfNeeded() {
|
||||
withState(loginViewModel) { state ->
|
||||
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
||||
if (state.loginMode.hasSso() && state.loginMode.ssoState().isFallback()) {
|
||||
// in this case we can prefetch (not other cases for privacy concerns)
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
|
@ -17,12 +17,13 @@
|
||||
package im.vector.app.features.login
|
||||
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.network.ssl.Fingerprint
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeServerConnectionConfigFactory @Inject constructor() {
|
||||
|
||||
fun create(url: String?): HomeServerConnectionConfig? {
|
||||
fun create(url: String?, fingerprint: Fingerprint? = null): HomeServerConnectionConfig? {
|
||||
if (url == null) {
|
||||
return null
|
||||
}
|
||||
@ -30,6 +31,13 @@ class HomeServerConnectionConfigFactory @Inject constructor() {
|
||||
return try {
|
||||
HomeServerConnectionConfig.Builder()
|
||||
.withHomeServerUri(url)
|
||||
.run {
|
||||
if (fingerprint == null) {
|
||||
this
|
||||
} else {
|
||||
withAllowedFingerPrints(listOf(fingerprint))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t)
|
||||
|
@ -37,7 +37,6 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
@ -100,13 +99,11 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSocialLoginButtons(state: LoginViewState) {
|
||||
views.loginSocialLoginButtons.mode = when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
|
||||
SignMode.SignIn,
|
||||
SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
|
||||
}
|
||||
private fun ssoMode(state: LoginViewState) = when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
|
||||
SignMode.SignIn,
|
||||
SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
@ -201,16 +198,13 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
|
||||
|
||||
if (state.loginMode is LoginMode.SsoAndPassword) {
|
||||
views.loginSocialLoginContainer.isVisible = true
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders?.sorted()
|
||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(provider: SsoIdentityProvider?) {
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = provider?.id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
views.loginSocialLoginButtons.render(state.loginMode.ssoState, ssoMode(state)) { provider ->
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = provider?.id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
} else {
|
||||
views.loginSocialLoginContainer.isVisible = false
|
||||
@ -272,7 +266,6 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
|
||||
|
||||
setupUi(state)
|
||||
setupAutoFill(state)
|
||||
setupSocialLoginButtons(state)
|
||||
setupButtons(state)
|
||||
|
||||
when (state.asyncLoginAction) {
|
||||
|
@ -18,22 +18,21 @@ package im.vector.app.features.login
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
|
||||
sealed class LoginMode : Parcelable { // Parcelable because persist state
|
||||
|
||||
@Parcelize object Unknown : LoginMode()
|
||||
@Parcelize object Password : LoginMode()
|
||||
@Parcelize data class Sso(val ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginMode()
|
||||
@Parcelize data class SsoAndPassword(val ssoIdentityProviders: List<SsoIdentityProvider>?) : LoginMode()
|
||||
@Parcelize data class Sso(val ssoState: SsoState) : LoginMode()
|
||||
@Parcelize data class SsoAndPassword(val ssoState: SsoState) : LoginMode()
|
||||
@Parcelize object Unsupported : LoginMode()
|
||||
}
|
||||
|
||||
fun LoginMode.ssoIdentityProviders(): List<SsoIdentityProvider>? {
|
||||
fun LoginMode.ssoState(): SsoState {
|
||||
return when (this) {
|
||||
is LoginMode.Sso -> ssoIdentityProviders
|
||||
is LoginMode.SsoAndPassword -> ssoIdentityProviders
|
||||
else -> null
|
||||
is LoginMode.Sso -> ssoState
|
||||
is LoginMode.SsoAndPassword -> ssoState
|
||||
else -> SsoState.Fallback
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import im.vector.app.features.login.SocialLoginButtonsView.Mode
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -73,16 +73,13 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
|
||||
when (state.loginMode) {
|
||||
is LoginMode.SsoAndPassword -> {
|
||||
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
|
||||
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()?.sorted()
|
||||
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(provider: SsoIdentityProvider?) {
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = provider?.id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
views.loginSignupSigninSocialLoginButtons.render(state.loginMode.ssoState(), Mode.MODE_CONTINUE) { provider ->
|
||||
loginViewModel.getSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
providerId = provider?.id
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
@ -223,7 +223,7 @@ class LoginViewModel @AssistedInject constructor(
|
||||
setState {
|
||||
copy(
|
||||
signMode = SignMode.SignIn,
|
||||
loginMode = LoginMode.Sso(action.ssoIdentityProviders),
|
||||
loginMode = LoginMode.Sso(action.ssoIdentityProviders.toSsoState()),
|
||||
homeServerUrlFromUser = action.homeServerUrl,
|
||||
homeServerUrl = action.homeServerUrl,
|
||||
deviceId = action.deviceId
|
||||
@ -816,8 +816,8 @@ class LoginViewModel @AssistedInject constructor(
|
||||
val loginMode = when {
|
||||
// SSO login is taken first
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState())
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState())
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
|
||||
else -> LoginMode.Unsupported
|
||||
}
|
||||
|
@ -160,8 +160,11 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
|
||||
}
|
||||
}
|
||||
|
||||
fun SocialLoginButtonsView.render(ssoProviders: List<SsoIdentityProvider>?, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) {
|
||||
fun SocialLoginButtonsView.render(state: SsoState, mode: SocialLoginButtonsView.Mode, listener: (SsoIdentityProvider?) -> Unit) {
|
||||
this.mode = mode
|
||||
this.ssoIdentityProviders = ssoProviders?.sorted()
|
||||
this.ssoIdentityProviders = when (state) {
|
||||
SsoState.Fallback -> null
|
||||
is SsoState.IdentityProviders -> state.providers.sorted()
|
||||
}
|
||||
this.listener = SocialLoginButtonsView.InteractionListener { listener(it) }
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.login
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
|
||||
sealed interface SsoState : Parcelable {
|
||||
@Parcelize
|
||||
data class IdentityProviders(val providers: List<SsoIdentityProvider>) : SsoState
|
||||
|
||||
@Parcelize
|
||||
object Fallback : SsoState
|
||||
|
||||
fun isFallback() = this == Fallback
|
||||
|
||||
fun providersOrNull() = when (this) {
|
||||
Fallback -> null
|
||||
is IdentityProviders -> providers.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
fun List<SsoIdentityProvider>?.toSsoState() = this
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let { SsoState.IdentityProviders(it) }
|
||||
?: SsoState.Fallback
|
@ -82,7 +82,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
||||
|
||||
data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction
|
||||
|
||||
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction
|
||||
data class UserAcceptCertificate(val fingerprint: Fingerprint, val retryAction: OnboardingAction) : OnboardingAction
|
||||
|
||||
object PersonalizeProfile : OnboardingAction
|
||||
data class UpdateDisplayName(val displayName: String) : OnboardingAction
|
||||
|
@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
||||
import org.matrix.android.sdk.api.failure.Failure as SdkFailure
|
||||
|
||||
/**
|
||||
* Transient events for Login.
|
||||
@ -29,6 +30,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
|
||||
data class Loading(val message: CharSequence? = null) : OnboardingViewEvents()
|
||||
data class Failure(val throwable: Throwable) : OnboardingViewEvents()
|
||||
data class DeeplinkAuthenticationFailure(val retryAction: OnboardingAction) : OnboardingViewEvents()
|
||||
data class UnrecognisedCertificateFailure(val retryAction: OnboardingAction, val cause: SdkFailure.UnrecognizedCertificateFailure) : OnboardingViewEvents()
|
||||
|
||||
object DisplayRegistrationFallback : OnboardingViewEvents()
|
||||
data class DisplayRegistrationStage(val stage: Stage) : OnboardingViewEvents()
|
||||
|
@ -60,7 +60,10 @@ import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||
import org.matrix.android.sdk.api.failure.isUnrecognisedCertificate
|
||||
import org.matrix.android.sdk.api.network.ssl.Fingerprint
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||
import timber.log.Timber
|
||||
@ -113,10 +116,6 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Store the last action, to redo it after user has trusted the untrusted certificate
|
||||
private var lastAction: OnboardingAction? = null
|
||||
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
|
||||
|
||||
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
|
||||
private val defaultHomeserverUrl = matrixOrgUrl
|
||||
|
||||
@ -146,9 +145,9 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
|
||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
||||
is OnboardingAction.HomeServerChange -> handleHomeserverChange(action)
|
||||
is OnboardingAction.UserNameEnteredAction -> handleUserNameEntered(action)
|
||||
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
|
||||
is AuthenticateAction -> handleAuthenticateAction(action)
|
||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||
is OnboardingAction.ResetPassword -> handleResetPassword(action)
|
||||
@ -221,11 +220,6 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun withAction(action: OnboardingAction, block: (OnboardingAction) -> Unit) {
|
||||
lastAction = action
|
||||
block(action)
|
||||
}
|
||||
|
||||
private fun handleAuthenticateAction(action: AuthenticateAction) {
|
||||
when (action) {
|
||||
is AuthenticateAction.Register -> handleRegisterWith(action.username, action.password, action.initialDeviceName)
|
||||
@ -276,20 +270,13 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) {
|
||||
// It happens when we get the login flow, or during direct authentication.
|
||||
// So alter the homeserver config and retrieve again the login flow
|
||||
when (val finalLastAction = lastAction) {
|
||||
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
|
||||
currentHomeServerConnectionConfig
|
||||
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
|
||||
?.let { startAuthenticationFlow(finalLastAction, it, serverTypeOverride = null) }
|
||||
}
|
||||
when (action.retryAction) {
|
||||
is OnboardingAction.HomeServerChange -> handleHomeserverChange(action.retryAction, fingerprint = action.fingerprint)
|
||||
is AuthenticateAction.LoginDirect ->
|
||||
handleDirectLogin(
|
||||
finalLastAction,
|
||||
HomeServerConnectionConfig.Builder()
|
||||
// Will be replaced by the task
|
||||
.withHomeServerUri("https://dummy.org")
|
||||
.withAllowedFingerPrints(listOf(action.fingerprint))
|
||||
.build()
|
||||
action.retryAction,
|
||||
// Will be replaced by the task
|
||||
homeServerConnectionConfigFactory.create("https://dummy.org", action.fingerprint)
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
@ -589,9 +576,19 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
currentJob = viewModelScope.launch {
|
||||
directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
|
||||
onSuccess = { onSessionCreated(it, authenticationDescription = AuthenticationDescription.Login) },
|
||||
onFailure = {
|
||||
onFailure = { error ->
|
||||
setState { copy(isLoading = false) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(it))
|
||||
when {
|
||||
error.isUnrecognisedCertificate() -> {
|
||||
_viewEvents.post(
|
||||
OnboardingViewEvents.UnrecognisedCertificateFailure(
|
||||
retryAction = action,
|
||||
cause = error as Failure.UnrecognizedCertificateFailure
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> _viewEvents.post(OnboardingViewEvents.Failure(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -682,8 +679,13 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange, serverTypeOverride: ServerType? = null, postAction: suspend () -> Unit = {}) {
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
|
||||
private fun handleHomeserverChange(
|
||||
action: OnboardingAction.HomeServerChange,
|
||||
serverTypeOverride: ServerType? = null,
|
||||
fingerprint: Fingerprint? = null,
|
||||
postAction: suspend () -> Unit = {},
|
||||
) {
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl, fingerprint)
|
||||
if (homeServerConnectionConfig == null) {
|
||||
// This is invalid
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
||||
@ -698,8 +700,6 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
serverTypeOverride: ServerType?,
|
||||
postAction: suspend () -> Unit = {},
|
||||
) {
|
||||
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
setState { copy(isLoading = true) }
|
||||
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
|
||||
@ -723,9 +723,10 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
retryAction = (trigger as OnboardingAction.HomeServerChange.SelectHomeServer).resetToDefaultUrl()
|
||||
)
|
||||
)
|
||||
else -> _viewEvents.post(
|
||||
OnboardingViewEvents.Failure(error)
|
||||
)
|
||||
error.isUnrecognisedCertificate() -> {
|
||||
_viewEvents.post(OnboardingViewEvents.UnrecognisedCertificateFailure(trigger, error as Failure.UnrecognizedCertificateFailure))
|
||||
}
|
||||
else -> _viewEvents.post(OnboardingViewEvents.Failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.onboarding
|
||||
|
||||
import im.vector.app.core.extensions.containsAllItems
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.toSsoState
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
|
||||
@ -50,8 +51,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
|
||||
)
|
||||
|
||||
private fun LoginFlowResult.findPreferredLoginMode() = when {
|
||||
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders)
|
||||
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders)
|
||||
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders.toSsoState())
|
||||
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders.toSsoState())
|
||||
supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
|
||||
else -> LoginMode.Unsupported
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewModel
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
|
||||
/**
|
||||
* Parent Fragment for all the login/registration screens.
|
||||
@ -68,6 +67,7 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
|
||||
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
|
||||
when (viewEvents) {
|
||||
is OnboardingViewEvents.Failure -> showFailure(viewEvents.throwable)
|
||||
is OnboardingViewEvents.UnrecognisedCertificateFailure -> showUnrecognizedCertificateFailure(viewEvents)
|
||||
else ->
|
||||
// This is handled by the Activity
|
||||
Unit
|
||||
@ -84,20 +84,20 @@ abstract class AbstractFtueAuthFragment<VB : ViewBinding> : VectorBaseFragment<V
|
||||
is CancellationException ->
|
||||
/* Ignore this error, user has cancelled the action */
|
||||
Unit
|
||||
is Failure.UnrecognizedCertificateFailure -> showUnrecognizedCertificateFailure(throwable)
|
||||
else -> onError(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) {
|
||||
private fun showUnrecognizedCertificateFailure(event: OnboardingViewEvents.UnrecognisedCertificateFailure) {
|
||||
// Ask the user to accept the certificate
|
||||
val cause = event.cause
|
||||
unrecognizedCertificateDialog.show(requireActivity(),
|
||||
failure.fingerprint,
|
||||
failure.url,
|
||||
cause.fingerprint,
|
||||
cause.url,
|
||||
object : UnrecognizedCertificateDialog.Callback {
|
||||
override fun onAccept() {
|
||||
// User accept the certificate
|
||||
viewModel.handle(OnboardingAction.UserAcceptCertificate(failure.fingerprint))
|
||||
viewModel.handle(OnboardingAction.UserAcceptCertificate(cause.fingerprint, event.retryAction))
|
||||
}
|
||||
|
||||
override fun onIgnore() {
|
||||
|
@ -26,7 +26,7 @@ import com.airbnb.mvrx.withState
|
||||
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.hasSso
|
||||
import im.vector.app.features.login.ssoIdentityProviders
|
||||
import im.vector.app.features.login.ssoState
|
||||
|
||||
abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthFragment<VB>() {
|
||||
|
||||
@ -88,7 +88,7 @@ abstract class AbstractSSOFtueAuthFragment<VB : ViewBinding> : AbstractFtueAuthF
|
||||
|
||||
private fun prefetchIfNeeded() {
|
||||
withState(viewModel) { state ->
|
||||
if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders().isNullOrEmpty()) {
|
||||
if (state.selectedHomeserver.preferredLoginMode.hasSso() && state.selectedHomeserver.preferredLoginMode.ssoState().isFallback()) {
|
||||
// in this case we can prefetch (not other cases for privacy concerns)
|
||||
viewModel.fetchSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
|
@ -38,13 +38,13 @@ import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.login.SsoState
|
||||
import im.vector.app.features.login.render
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -125,11 +125,11 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||
when (state.selectedHomeserver.preferredLoginMode) {
|
||||
is LoginMode.SsoAndPassword -> {
|
||||
showUsernamePassword()
|
||||
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState)
|
||||
}
|
||||
is LoginMode.Sso -> {
|
||||
hideUsernamePassword()
|
||||
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState)
|
||||
}
|
||||
else -> {
|
||||
showUsernamePassword()
|
||||
@ -138,10 +138,10 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
|
||||
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
|
||||
views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible
|
||||
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
|
||||
private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) {
|
||||
views.ssoGroup.isVisible = true
|
||||
views.ssoButtonsHeader.isVisible = isUsernameAndPasswordVisible()
|
||||
views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
|
||||
viewModel.fetchSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = deviceId,
|
||||
@ -163,6 +163,8 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||
views.loginEntryGroup.isVisible = true
|
||||
}
|
||||
|
||||
private fun isUsernameAndPasswordVisible() = views.loginEntryGroup.isVisible
|
||||
|
||||
private fun setupAutoFill() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
views.loginInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
|
||||
|
@ -44,6 +44,7 @@ import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.login.SsoState
|
||||
import im.vector.app.features.login.render
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
||||
@ -51,7 +52,6 @@ import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
||||
@ -205,14 +205,14 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||
}
|
||||
|
||||
when (state.selectedHomeserver.preferredLoginMode) {
|
||||
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoState)
|
||||
else -> hideSsoProviders()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
|
||||
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
|
||||
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider ->
|
||||
private fun renderSsoProviders(deviceId: String?, ssoState: SsoState) {
|
||||
views.ssoGroup.isVisible = true
|
||||
views.ssoButtons.render(ssoState, SocialLoginButtonsView.Mode.MODE_CONTINUE) { provider ->
|
||||
viewModel.fetchSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = deviceId,
|
||||
|
@ -37,7 +37,8 @@ import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.login.SocialLoginButtonsView.Mode
|
||||
import im.vector.app.features.login.render
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
||||
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
||||
@ -111,13 +111,11 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSocialLoginButtons(state: OnboardingViewState) {
|
||||
views.loginSocialLoginButtons.mode = when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> SocialLoginButtonsView.Mode.MODE_SIGN_UP
|
||||
SignMode.SignIn,
|
||||
SignMode.SignInWithMatrixId -> SocialLoginButtonsView.Mode.MODE_SIGN_IN
|
||||
}
|
||||
private fun ssoMode(state: OnboardingViewState) = when (state.signMode) {
|
||||
SignMode.Unknown -> error("developer error")
|
||||
SignMode.SignUp -> Mode.MODE_SIGN_UP
|
||||
SignMode.SignIn,
|
||||
SignMode.SignInWithMatrixId -> Mode.MODE_SIGN_IN
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
@ -215,16 +213,13 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
|
||||
|
||||
if (state.selectedHomeserver.preferredLoginMode is LoginMode.SsoAndPassword) {
|
||||
views.loginSocialLoginContainer.isVisible = true
|
||||
views.loginSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders?.sorted()
|
||||
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(provider: SsoIdentityProvider?) {
|
||||
viewModel.fetchSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
provider = provider
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
views.loginSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, ssoMode(state)) { provider ->
|
||||
viewModel.fetchSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
provider = provider
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
} else {
|
||||
views.loginSocialLoginContainer.isVisible = false
|
||||
@ -305,7 +300,6 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
|
||||
|
||||
setupUi(state)
|
||||
setupAutoFill(state)
|
||||
setupSocialLoginButtons(state)
|
||||
setupButtons(state)
|
||||
|
||||
if (state.isLoading) {
|
||||
|
@ -30,11 +30,10 @@ import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SSORedirectRouterActivity
|
||||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.login.SocialLoginButtonsView
|
||||
import im.vector.app.features.login.ssoIdentityProviders
|
||||
import im.vector.app.features.login.SocialLoginButtonsView.Mode
|
||||
import im.vector.app.features.login.render
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@ -80,16 +79,13 @@ class FtueAuthSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOF
|
||||
when (state.selectedHomeserver.preferredLoginMode) {
|
||||
is LoginMode.SsoAndPassword -> {
|
||||
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
|
||||
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders()?.sorted()
|
||||
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
|
||||
override fun onProviderSelected(provider: SsoIdentityProvider?) {
|
||||
viewModel.fetchSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
provider = provider
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
views.loginSignupSigninSocialLoginButtons.render(state.selectedHomeserver.preferredLoginMode.ssoState, Mode.MODE_CONTINUE) { provider ->
|
||||
viewModel.fetchSsoUrl(
|
||||
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
|
||||
deviceId = state.deviceId,
|
||||
provider = provider
|
||||
)
|
||||
?.let { openInCustomTab(it) }
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
@ -202,6 +202,7 @@ class FtueAuthVariant(
|
||||
openMsisdnConfirmation(viewEvents.msisdn)
|
||||
}
|
||||
is OnboardingViewEvents.Failure,
|
||||
is OnboardingViewEvents.UnrecognisedCertificateFailure,
|
||||
is OnboardingViewEvents.Loading ->
|
||||
// This is handled by the Fragments
|
||||
Unit
|
||||
|
@ -78,6 +78,7 @@ class BugReporter @Inject constructor(
|
||||
private val systemLocaleProvider: SystemLocaleProvider,
|
||||
private val matrix: Matrix,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val processInfo: ProcessInfo,
|
||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||
) {
|
||||
var inMultiWindowMode = false
|
||||
@ -499,10 +500,26 @@ class BugReporter @Inject constructor(
|
||||
*/
|
||||
fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) {
|
||||
screenshot = takeScreenshot(activity)
|
||||
matrix.debugService().logDbUsageInfo()
|
||||
logDbInfo()
|
||||
logProcessInfo()
|
||||
logOtherInfo()
|
||||
activity.startActivity(BugReportActivity.intent(activity, reportType))
|
||||
}
|
||||
|
||||
private fun logOtherInfo() {
|
||||
Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState())
|
||||
}
|
||||
|
||||
private fun logDbInfo() {
|
||||
val dbInfo = matrix.debugService().getDbUsageInfo()
|
||||
Timber.i(dbInfo)
|
||||
}
|
||||
|
||||
private fun logProcessInfo() {
|
||||
val pInfo = processInfo.getInfo()
|
||||
Timber.i(pInfo)
|
||||
}
|
||||
|
||||
private fun rageShakeAppNameForReport(reportType: ReportType): String {
|
||||
// As per https://github.com/matrix-org/rageshake
|
||||
// app: Identifier for the application (eg 'riot-web').
|
||||
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.rageshake
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import java.lang.reflect.Method
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProcessInfo @Inject constructor() {
|
||||
fun getInfo() = buildString {
|
||||
append("===========================================\n")
|
||||
append("* PROCESS INFO *\n")
|
||||
append("===========================================\n")
|
||||
val processId = Process.myPid()
|
||||
append("ProcessId: $processId\n")
|
||||
append("ProcessName: ${getProcessName()}\n")
|
||||
append(getThreadInfo())
|
||||
append("===========================================\n")
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun getProcessName(): String? {
|
||||
return if (Build.VERSION.SDK_INT >= 28) {
|
||||
Application.getProcessName()
|
||||
} else {
|
||||
try {
|
||||
val activityThread = Class.forName("android.app.ActivityThread")
|
||||
val getProcessName: Method = activityThread.getDeclaredMethod("currentProcessName")
|
||||
getProcessName.invoke(null) as? String
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreadInfo() = buildString {
|
||||
append("Thread activeCount: ${Thread.activeCount()}\n")
|
||||
Thread.getAllStackTraces().keys
|
||||
.sortedBy { it.name }
|
||||
.forEach { thread -> append(thread.getInfo()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Thread.getInfo() = buildString {
|
||||
append("Thread '$name':")
|
||||
append(" id: $id")
|
||||
append(" priority: $priority")
|
||||
append(" group name: ${threadGroup?.name ?: "null"}")
|
||||
append(" state: $state")
|
||||
append(" isAlive: $isAlive")
|
||||
append(" isDaemon: $isDaemon")
|
||||
append(" isInterrupted: $isInterrupted")
|
||||
append("\n")
|
||||
}
|
@ -63,7 +63,7 @@ class SoftLogoutFragment @Inject constructor(
|
||||
LoginAction.SetupSsoForSessionRecovery(
|
||||
softLogoutViewState.homeServerUrl,
|
||||
softLogoutViewState.deviceId,
|
||||
mode.ssoIdentityProviders
|
||||
mode.ssoState.providersOrNull()
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -72,7 +72,7 @@ class SoftLogoutFragment @Inject constructor(
|
||||
LoginAction.SetupSsoForSessionRecovery(
|
||||
softLogoutViewState.homeServerUrl,
|
||||
softLogoutViewState.deviceId,
|
||||
mode.ssoIdentityProviders
|
||||
mode.ssoState.providersOrNull()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.hasUnsavedKeys
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.toSsoState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
@ -115,8 +116,8 @@ class SoftLogoutViewModel @AssistedInject constructor(
|
||||
val loginMode = when {
|
||||
// SSO login is taken first
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) &&
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders)
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders.toSsoState())
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders.toSsoState())
|
||||
data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password
|
||||
else -> LoginMode.Unsupported
|
||||
}
|
||||
|
10
vector/src/main/res/drawable/ic_chat.xml
Normal file
10
vector/src/main/res/drawable/ic_chat.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12.283,21.44C17.649,21.44 22,17.088 22,11.719C22,6.351 17.649,1.999 12.283,1.999C6.916,1.999 2.566,6.351 2.566,11.719C2.566,13.223 2.907,14.648 3.517,15.918L2.045,20.705C1.808,21.474 2.531,22.194 3.299,21.953L8.046,20.47C9.326,21.091 10.764,21.44 12.283,21.44Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
10
vector/src/main/res/drawable/ic_room_add.xml
Normal file
10
vector/src/main/res/drawable/ic_room_add.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M17.048,4.105C17.107,3.556 16.708,3.064 16.159,3.006C15.61,2.948 15.118,3.346 15.059,3.895L14.668,7.602H9.613L9.983,4.105C10.041,3.556 9.642,3.064 9.093,3.006C8.544,2.948 8.052,3.346 7.994,3.895L7.602,7.602H4.834C4.281,7.602 3.834,8.05 3.834,8.602C3.834,9.154 4.281,9.602 4.834,9.602H7.39L6.872,14.505H4C3.448,14.505 3,14.953 3,15.505C3,16.058 3.448,16.506 4,16.506H6.661L6.331,19.622C6.273,20.171 6.671,20.663 7.221,20.721C7.77,20.779 8.262,20.381 8.32,19.832L8.589,17.288C8.319,16.818 8.165,16.274 8.165,15.693C8.165,14.871 8.474,14.122 8.984,13.555L9.401,9.602H12.959C13.538,8.776 14.497,8.235 15.583,8.235C16.669,8.235 17.628,8.776 18.207,9.602H19.379C19.931,9.602 20.379,9.154 20.379,8.602C20.379,8.05 19.931,7.602 19.379,7.602H16.679L17.048,4.105ZM15.583,10.382C16.135,10.382 16.583,10.83 16.583,11.382V14.693H19.852C20.404,14.693 20.852,15.141 20.852,15.693C20.852,16.246 20.404,16.694 19.852,16.694H16.583V20.004C16.583,20.557 16.135,21.004 15.583,21.004C15.031,21.004 14.583,20.557 14.583,20.004V16.694H11.313C10.76,16.694 10.313,16.246 10.313,15.693C10.313,15.141 10.76,14.693 11.313,14.693H14.583V11.382C14.583,10.83 15.031,10.382 15.583,10.382Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
14
vector/src/main/res/drawable/ic_room_explore.xml
Normal file
14
vector/src/main/res/drawable/ic_room_explore.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16.647,3.006C17.196,3.065 17.593,3.558 17.533,4.108L17.147,7.684H19.998C20.55,7.684 20.998,8.132 20.998,8.684C20.998,9.236 20.55,9.684 19.998,9.684H16.931L16.8,10.891C16.058,10.967 15.356,11.181 14.722,11.508L14.919,9.684H9.582L9.039,14.708H11.898C11.67,15.332 11.545,16.006 11.544,16.708H8.822L8.455,20.111C8.395,20.66 7.902,21.057 7.353,20.997C6.804,20.938 6.407,20.445 6.466,19.896L6.811,16.708H3.999C3.447,16.708 2.999,16.26 2.999,15.708C2.999,15.156 3.447,14.708 3.999,14.708H7.027L7.57,9.684H4.864C4.312,9.684 3.864,9.236 3.864,8.684C3.864,8.132 4.312,7.684 4.864,7.684H7.786L8.196,3.893C8.255,3.344 8.748,2.947 9.298,3.006C9.847,3.065 10.244,3.558 10.184,4.108L9.798,7.684H15.135L15.545,3.893C15.604,3.344 16.097,2.947 16.647,3.006Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M19.003,16.765C19.003,17.817 18.151,18.669 17.1,18.669C16.048,18.669 15.196,17.817 15.196,16.765C15.196,15.714 16.048,14.862 17.1,14.862C18.151,14.862 19.003,15.714 19.003,16.765ZM20.332,18.698C20.67,18.133 20.865,17.472 20.865,16.765C20.865,14.686 19.179,13 17.1,13C15.02,13 13.334,14.686 13.334,16.765C13.334,18.845 15.02,20.531 17.1,20.531C17.806,20.531 18.467,20.336 19.032,19.998C19.062,20.038 19.094,20.077 19.131,20.114L20.745,21.727C21.108,22.091 21.698,22.091 22.061,21.727C22.425,21.364 22.425,20.774 22.061,20.411L20.448,18.797C20.411,18.76 20.372,18.728 20.332,18.698Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/rootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/home_layout_preferences"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/home_layout_settings_recents"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:checked="true"
|
||||
android:text="@string/home_layout_preferences_recents" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/home_layout_settings_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:checked="true"
|
||||
android:text="@string/home_layout_preferences_filters" />
|
||||
|
||||
<TextView
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:text="@string/home_layout_preferences_sort_by"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/home_layout_settings_sort_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/home_layout_settings_sort_activity"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/home_layout_preferences_sort_activity"
|
||||
android:textColor="?vctr_content_primary" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/home_layout_settings_sort_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/home_layout_preferences_sort_name"
|
||||
android:textColor="?vctr_content_primary" />
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/start_chat"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/start_chat"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textSize="16sp"
|
||||
app:drawableStartCompat="@drawable/ic_chat" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/create_room"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/create_room"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textSize="16sp"
|
||||
app:drawableStartCompat="@drawable/ic_room_add" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/explore_rooms"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawablePadding="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
android:text="@string/explore_rooms"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textSize="16sp"
|
||||
app:drawableStartCompat="@drawable/ic_room_explore" />
|
||||
|
||||
</LinearLayout>
|
@ -3,6 +3,10 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_layout_settings"
|
||||
android:title="@string/home_layout_preferences"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_invite_friends"
|
||||
android:title="@string/invite_friends"
|
||||
@ -37,6 +41,6 @@
|
||||
android:icon="@drawable/ic_home_search"
|
||||
android:title="@string/home_filter_placeholder_home"
|
||||
app:iconTint="?vctr_content_secondary"
|
||||
app:showAsAction="always" />
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
||||
|
@ -1,9 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_home_mark_all_as_read"
|
||||
android:icon="@drawable/ic_material_done"
|
||||
android:title="@string/action_mark_all_as_read" />
|
||||
|
||||
</menu>
|
||||
<item
|
||||
android:id="@+id/menu_home_dialpad"
|
||||
android:title="@string/call_dial_pad_title"
|
||||
android:visible="false"
|
||||
app:showAsAction="never"
|
||||
tools:visible="true" />
|
||||
</menu>
|
||||
|
@ -137,7 +137,10 @@
|
||||
|
||||
<!-- Home Screen -->
|
||||
<string name="all_chats">All Chats</string>
|
||||
<string name="start_chat">Start Chat</string>
|
||||
<string name="create_room">Create Room</string>
|
||||
<string name="change_space">Change Space</string>
|
||||
<string name="explore_rooms">Explore Rooms</string>
|
||||
|
||||
<!-- Last seen time -->
|
||||
|
||||
@ -424,6 +427,15 @@
|
||||
|
||||
<!-- Home screen -->
|
||||
<string name="home_filter_placeholder_home">Filter room names</string>
|
||||
<string name="home_layout_preferences">Layout preferences</string>
|
||||
|
||||
|
||||
<!-- Home screen layout settings -->
|
||||
<string name="home_layout_preferences_filters">Show filters</string>
|
||||
<string name="home_layout_preferences_recents">Show recents</string>
|
||||
<string name="home_layout_preferences_sort_by">Sort by</string>
|
||||
<string name="home_layout_preferences_sort_activity">Activity</string>
|
||||
<string name="home_layout_preferences_sort_name">A - Z</string>
|
||||
|
||||
<!-- Home fragment -->
|
||||
<string name="invitations_header">Invites</string>
|
||||
|
@ -48,6 +48,7 @@ import im.vector.app.test.fakes.FakeVectorOverrides
|
||||
import im.vector.app.test.fakes.toTestString
|
||||
import im.vector.app.test.fixtures.a401ServerError
|
||||
import im.vector.app.test.fixtures.aHomeServerCapabilities
|
||||
import im.vector.app.test.fixtures.anUnrecognisedCertificateError
|
||||
import im.vector.app.test.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
@ -58,6 +59,7 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.network.ssl.Fingerprint
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
|
||||
@ -65,10 +67,12 @@ private const val A_DISPLAY_NAME = "a display name"
|
||||
private const val A_PICTURE_FILENAME = "a-picture.png"
|
||||
private val A_SERVER_ERROR = a401ServerError()
|
||||
private val AN_ERROR = RuntimeException("an error!")
|
||||
private val AN_UNRECOGNISED_CERTIFICATE_ERROR = anUnrecognisedCertificateError()
|
||||
private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration
|
||||
private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenValidated(delayMillis = -1L)
|
||||
private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.SendAgainThreePid
|
||||
private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
|
||||
private val A_FINGERPRINT = Fingerprint(ByteArray(1), Fingerprint.HashType.SHA1)
|
||||
private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationActionHandler.Result.NextStage(Stage.Dummy(mandatory = true))
|
||||
private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name")
|
||||
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
|
||||
@ -320,6 +324,25 @@ class OnboardingViewModelTest {
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given has sign in with matrix id sign mode, when handling login or register action fails with certificate error, then emits error`() = runTest {
|
||||
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
|
||||
fakeDirectLoginUseCase.givenFailureResult(A_DIRECT_LOGIN, config = null, cause = AN_UNRECOGNISED_CERTIFICATE_ERROR)
|
||||
givenInitialisesSession(fakeSession)
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(A_DIRECT_LOGIN)
|
||||
|
||||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.UnrecognisedCertificateFailure(A_DIRECT_LOGIN, AN_UNRECOGNISED_CERTIFICATE_ERROR))
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runTest {
|
||||
givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
|
||||
@ -406,7 +429,7 @@ class OnboardingViewModelTest {
|
||||
@Test
|
||||
fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest {
|
||||
fakeContext.givenHasConnection()
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenHomeserverUnavailable(A_HOMESERVER_CONFIG)
|
||||
val test = viewModel.test()
|
||||
|
||||
@ -548,6 +571,44 @@ class OnboardingViewModelTest {
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when editing homeserver errors with certificate error, then emits error`() = runTest {
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenErrors(A_HOMESERVER_CONFIG, AN_UNRECOGNISED_CERTIFICATE_ERROR)
|
||||
val editAction = OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL)
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(editAction)
|
||||
|
||||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.UnrecognisedCertificateFailure(editAction, AN_UNRECOGNISED_CERTIFICATE_ERROR))
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when selecting homeserver errors with certificate error, then emits error`() = runTest {
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, fingerprint = null, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenErrors(A_HOMESERVER_CONFIG, AN_UNRECOGNISED_CERTIFICATE_ERROR)
|
||||
val selectAction = OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL)
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(selectAction)
|
||||
|
||||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.UnrecognisedCertificateFailure(selectAction, AN_UNRECOGNISED_CERTIFICATE_ERROR))
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given unavailable full matrix id, when a register username is entered, then emits availability error`() = runTest {
|
||||
viewModelWith(initialRegistrationState("ignored-url"))
|
||||
@ -724,6 +785,76 @@ class OnboardingViewModelTest {
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given in sign in mode, when accepting user certificate with SelectHomeserver retry action, then emits OnHomeserverEdited`() = runTest {
|
||||
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignIn))
|
||||
val test = viewModel.test()
|
||||
fakeVectorFeatures.givenCombinedLoginEnabled()
|
||||
givenCanSuccessfullyUpdateHomeserver(
|
||||
A_HOMESERVER_URL,
|
||||
SELECTED_HOMESERVER_STATE,
|
||||
config = A_HOMESERVER_CONFIG.copy(allowedFingerprints = listOf(A_FINGERPRINT)),
|
||||
fingerprint = A_FINGERPRINT,
|
||||
)
|
||||
|
||||
viewModel.handle(OnboardingAction.UserAcceptCertificate(A_FINGERPRINT, OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL)))
|
||||
|
||||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) },
|
||||
{ copy(signMode = SignMode.SignIn) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OpenCombinedLogin)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given in sign up mode, when accepting user certificate with EditHomeserver retry action, then emits OnHomeserverEdited`() = runTest {
|
||||
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||
givenCanSuccessfullyUpdateHomeserver(
|
||||
A_HOMESERVER_URL,
|
||||
SELECTED_HOMESERVER_STATE,
|
||||
config = A_HOMESERVER_CONFIG.copy(allowedFingerprints = listOf(A_FINGERPRINT)),
|
||||
fingerprint = A_FINGERPRINT,
|
||||
)
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.UserAcceptCertificate(A_FINGERPRINT, OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL)))
|
||||
|
||||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) },
|
||||
{ copy(isLoading = false) }
|
||||
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnHomeserverEdited)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given DirectLogin retry action, when accepting user certificate, then logs in directly`() = runTest {
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor("https://dummy.org", A_FINGERPRINT, A_HOMESERVER_CONFIG)
|
||||
fakeDirectLoginUseCase.givenSuccessResult(A_DIRECT_LOGIN, config = A_HOMESERVER_CONFIG, result = fakeSession)
|
||||
givenInitialisesSession(fakeSession)
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.UserAcceptCertificate(A_FINGERPRINT, A_DIRECT_LOGIN))
|
||||
|
||||
test
|
||||
.assertStatesChanges(
|
||||
initialState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnAccountSignedIn)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given can successfully start password reset, when resetting password, then emits confirmation email sent`() = runTest {
|
||||
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
|
||||
@ -991,15 +1122,20 @@ class OnboardingViewModelTest {
|
||||
fakeRegistrationActionHandler.givenResultsFor(results)
|
||||
}
|
||||
|
||||
private fun givenCanSuccessfullyUpdateHomeserver(homeserverUrl: String, resultingState: SelectedHomeserverState) {
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
||||
private fun givenCanSuccessfullyUpdateHomeserver(
|
||||
homeserverUrl: String,
|
||||
resultingState: SelectedHomeserverState,
|
||||
config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
|
||||
fingerprint: Fingerprint? = null,
|
||||
) {
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
|
||||
fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
||||
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
|
||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||
fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
|
||||
}
|
||||
|
||||
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG)
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint = null, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
||||
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
|
||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.app.features.onboarding
|
||||
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.SsoState
|
||||
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
|
||||
import im.vector.app.test.fakes.FakeAuthenticationService
|
||||
import im.vector.app.test.fakes.FakeUri
|
||||
@ -32,7 +33,11 @@ import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
|
||||
private const val A_DECLARED_HOMESERVER_URL = "https://foo.bar"
|
||||
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(homeServerUri = FakeUri().instance)
|
||||
private val SSO_IDENTITY_PROVIDERS = emptyList<SsoIdentityProvider>()
|
||||
private val FALLBACK_SSO_IDENTITY_PROVIDERS = emptyList<SsoIdentityProvider>()
|
||||
private val SSO_IDENTITY_PROVIDERS = listOf(SsoIdentityProvider(id = "id", "name", null, "sso-brand"))
|
||||
private val SSO_LOGIN_TYPE = listOf(LoginFlowTypes.SSO)
|
||||
private val SSO_AND_PASSWORD_LOGIN_TYPES = listOf(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD)
|
||||
private val PASSWORD_LOGIN_TYPE = listOf(LoginFlowTypes.PASSWORD)
|
||||
|
||||
class StartAuthenticationFlowUseCaseTest {
|
||||
|
||||
@ -47,7 +52,7 @@ class StartAuthenticationFlowUseCaseTest {
|
||||
|
||||
@Test
|
||||
fun `given empty login result when starting authentication flow then returns empty result`() = runTest {
|
||||
val loginResult = aLoginResult()
|
||||
val loginResult = aLoginResult(supportedLoginTypes = emptyList())
|
||||
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
|
||||
|
||||
val result = useCase.execute(A_HOMESERVER_CONFIG)
|
||||
@ -57,55 +62,81 @@ class StartAuthenticationFlowUseCaseTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given login supports SSO and Password when starting authentication flow then prefers SsoAndPassword`() = runTest {
|
||||
val supportedLoginTypes = listOf(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD)
|
||||
val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
|
||||
fun `given empty sso providers and login supports SSO and Password when starting authentication flow then prefers fallback SsoAndPassword`() = runTest {
|
||||
val loginResult = aLoginResult(supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, ssoProviders = emptyList())
|
||||
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
|
||||
|
||||
val result = useCase.execute(A_HOMESERVER_CONFIG)
|
||||
|
||||
result shouldBeEqualTo expectedResult(
|
||||
supportedLoginTypes = supportedLoginTypes,
|
||||
preferredLoginMode = LoginMode.SsoAndPassword(SSO_IDENTITY_PROVIDERS),
|
||||
supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES,
|
||||
preferredLoginMode = LoginMode.SsoAndPassword(SsoState.Fallback),
|
||||
)
|
||||
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given login supports SSO when starting authentication flow then prefers Sso`() = runTest {
|
||||
val supportedLoginTypes = listOf(LoginFlowTypes.SSO)
|
||||
val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
|
||||
fun `given sso providers and login supports SSO and Password when starting authentication flow then prefers SsoAndPassword`() = runTest {
|
||||
val loginResult = aLoginResult(supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES, ssoProviders = SSO_IDENTITY_PROVIDERS)
|
||||
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
|
||||
|
||||
val result = useCase.execute(A_HOMESERVER_CONFIG)
|
||||
|
||||
result shouldBeEqualTo expectedResult(
|
||||
supportedLoginTypes = supportedLoginTypes,
|
||||
preferredLoginMode = LoginMode.Sso(SSO_IDENTITY_PROVIDERS),
|
||||
supportedLoginTypes = SSO_AND_PASSWORD_LOGIN_TYPES,
|
||||
preferredLoginMode = LoginMode.SsoAndPassword(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)),
|
||||
)
|
||||
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given empty sso providers and login supports SSO when starting authentication flow then prefers fallback Sso`() = runTest {
|
||||
val loginResult = aLoginResult(supportedLoginTypes = SSO_LOGIN_TYPE, ssoProviders = emptyList())
|
||||
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
|
||||
|
||||
val result = useCase.execute(A_HOMESERVER_CONFIG)
|
||||
|
||||
result shouldBeEqualTo expectedResult(
|
||||
supportedLoginTypes = SSO_LOGIN_TYPE,
|
||||
preferredLoginMode = LoginMode.Sso(SsoState.Fallback),
|
||||
)
|
||||
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given identity providers and login supports SSO when starting authentication flow then prefers Sso`() = runTest {
|
||||
val loginResult = aLoginResult(supportedLoginTypes = SSO_LOGIN_TYPE, ssoProviders = SSO_IDENTITY_PROVIDERS)
|
||||
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
|
||||
|
||||
val result = useCase.execute(A_HOMESERVER_CONFIG)
|
||||
|
||||
result shouldBeEqualTo expectedResult(
|
||||
supportedLoginTypes = SSO_LOGIN_TYPE,
|
||||
preferredLoginMode = LoginMode.Sso(SsoState.IdentityProviders(SSO_IDENTITY_PROVIDERS)),
|
||||
)
|
||||
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given login supports Password when starting authentication flow then prefers Password`() = runTest {
|
||||
val supportedLoginTypes = listOf(LoginFlowTypes.PASSWORD)
|
||||
val loginResult = aLoginResult(supportedLoginTypes = supportedLoginTypes)
|
||||
val loginResult = aLoginResult(supportedLoginTypes = PASSWORD_LOGIN_TYPE)
|
||||
fakeAuthenticationService.givenLoginFlow(A_HOMESERVER_CONFIG, loginResult)
|
||||
|
||||
val result = useCase.execute(A_HOMESERVER_CONFIG)
|
||||
|
||||
result shouldBeEqualTo expectedResult(
|
||||
supportedLoginTypes = supportedLoginTypes,
|
||||
supportedLoginTypes = PASSWORD_LOGIN_TYPE,
|
||||
preferredLoginMode = LoginMode.Password,
|
||||
)
|
||||
verifyClearsAndThenStartsLogin(A_HOMESERVER_CONFIG)
|
||||
}
|
||||
|
||||
private fun aLoginResult(
|
||||
supportedLoginTypes: List<String> = emptyList()
|
||||
supportedLoginTypes: List<String>,
|
||||
ssoProviders: List<SsoIdentityProvider> = FALLBACK_SSO_IDENTITY_PROVIDERS
|
||||
) = LoginFlowResult(
|
||||
supportedLoginTypes = supportedLoginTypes,
|
||||
ssoIdentityProviders = SSO_IDENTITY_PROVIDERS,
|
||||
ssoIdentityProviders = ssoProviders,
|
||||
isLoginAndRegistrationSupported = true,
|
||||
homeServerUrl = A_DECLARED_HOMESERVER_URL,
|
||||
isOutdatedHomeserver = false,
|
||||
|
@ -20,11 +20,12 @@ import im.vector.app.features.login.HomeServerConnectionConfigFactory
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.network.ssl.Fingerprint
|
||||
|
||||
class FakeHomeServerConnectionConfigFactory {
|
||||
val instance: HomeServerConnectionConfigFactory = mockk()
|
||||
|
||||
fun givenConfigFor(url: String, config: HomeServerConnectionConfig) {
|
||||
every { instance.create(url) } returns config
|
||||
fun givenConfigFor(url: String, fingerprint: Fingerprint? = null, config: HomeServerConnectionConfig) {
|
||||
every { instance.create(url, fingerprint) } returns config
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,10 @@ class FakeStartAuthenticationFlowUseCase {
|
||||
coEvery { instance.execute(config) } returns result
|
||||
}
|
||||
|
||||
fun givenErrors(config: HomeServerConnectionConfig, error: Throwable) {
|
||||
coEvery { instance.execute(config) } throws error
|
||||
}
|
||||
|
||||
fun givenHomeserverUnavailable(config: HomeServerConnectionConfig) {
|
||||
coEvery { instance.execute(config) } throws aHomeserverUnavailableError()
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package im.vector.app.test.fixtures
|
||||
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.network.ssl.Fingerprint
|
||||
import java.net.UnknownHostException
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
@ -38,3 +39,5 @@ fun aLoginEmailUnknownError() = Failure.ServerError(
|
||||
)
|
||||
|
||||
fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException())
|
||||
|
||||
fun anUnrecognisedCertificateError() = Failure.UnrecognizedCertificateFailure("a-url", Fingerprint(ByteArray(1), Fingerprint.HashType.SHA1))
|
||||
|
Loading…
x
Reference in New Issue
Block a user