BillCarsonFr/JsonViewer
+
+ Copyright (C) 2018 stfalcon.com
+
Apache License
diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
index ad40980349..241240d7e3 100644
--- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
@@ -32,8 +32,6 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen
-import com.github.piasy.biv.BigImageViewer
-import com.github.piasy.biv.loader.glide.GlideImageLoader
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.AuthenticationService
@@ -44,16 +42,13 @@ import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.rx.RxConfig
-import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.disclaimer.doNotShowDisclaimerDialog
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.NotificationUtils
-import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
-import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper
@@ -80,16 +75,13 @@ class VectorApplication :
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
- @Inject lateinit var sessionListener: SessionListener
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
- @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager
- @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent
@@ -115,7 +107,6 @@ class VectorApplication :
logInfo()
LazyThreeTen.init(this)
- BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
@@ -142,8 +133,7 @@ class VectorApplication :
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
- lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
- lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
+ lastAuthenticatedSession.configureAndStart(applicationContext)
}
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
diff --git a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt
index 967d7d638d..37c07b8293 100644
--- a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt
+++ b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 New Vector Ltd
+ * Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.content.withStyledAttributes
import im.vector.riotx.R
import kotlin.math.abs
@@ -67,19 +68,19 @@ class PercentViewBehavior(context: Context, attrs: AttributeSet) : Coo
private var isPrepared: Boolean = false
init {
- val a = context.obtainStyledAttributes(attrs, R.styleable.PercentViewBehavior)
- dependViewId = a.getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0)
- dependType = a.getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH)
- dependTarget = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT)
- targetX = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT)
- targetY = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT)
- targetWidth = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT)
- targetHeight = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT)
- targetBackgroundColor = a.getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT)
- targetAlpha = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT)
- targetRotateX = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT)
- targetRotateY = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT)
- a.recycle()
+ context.withStyledAttributes(attrs, R.styleable.PercentViewBehavior) {
+ dependViewId = getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0)
+ dependType = getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH)
+ dependTarget = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT)
+ targetX = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT)
+ targetY = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT)
+ targetWidth = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT)
+ targetHeight = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT)
+ targetBackgroundColor = getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT)
+ targetAlpha = getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT)
+ targetRotateX = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT)
+ targetRotateY = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT)
+ }
}
private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) {
diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
new file mode 100644
index 0000000000..fd23e495b9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.contacts
+
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.ContactsContract
+import androidx.annotation.WorkerThread
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.system.measureTimeMillis
+
+class ContactsDataSource @Inject constructor(
+ private val context: Context
+) {
+
+ /**
+ * Will return a list of contact from the contacts book of the device, with at least one email or phone.
+ * If both param are false, you will get en empty list.
+ * Note: The return list does not contain any matrixId.
+ */
+ @WorkerThread
+ fun getContacts(
+ withEmails: Boolean,
+ withMsisdn: Boolean
+ ): List {
+ val map = mutableMapOf()
+ val contentResolver = context.contentResolver
+
+ measureTimeMillis {
+ contentResolver.query(
+ ContactsContract.Contacts.CONTENT_URI,
+ arrayOf(
+ ContactsContract.Contacts._ID,
+ ContactsContract.Data.DISPLAY_NAME,
+ ContactsContract.Data.PHOTO_URI
+ ),
+ null,
+ null,
+ // Sort by Display name
+ ContactsContract.Data.DISPLAY_NAME
+ )
+ ?.use { cursor ->
+ if (cursor.count > 0) {
+ while (cursor.moveToNext()) {
+ val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
+ val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
+
+ val mappedContactBuilder = MappedContactBuilder(
+ id = id,
+ displayName = displayName
+ )
+
+ cursor.getString(ContactsContract.Data.PHOTO_URI)
+ ?.let { Uri.parse(it) }
+ ?.let { mappedContactBuilder.photoURI = it }
+
+ map[id] = mappedContactBuilder
+ }
+ }
+ }
+
+ // Get the phone numbers
+ if (withMsisdn) {
+ contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ arrayOf(
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
+ ContactsContract.CommonDataKinds.Phone.NUMBER
+ ),
+ null,
+ null,
+ null)
+ ?.use { innerCursor ->
+ while (innerCursor.moveToNext()) {
+ val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
+ ?.let { map[it] }
+ ?: continue
+ innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
+ ?.let {
+ mappedContactBuilder.msisdns.add(
+ MappedMsisdn(
+ phoneNumber = it,
+ matrixId = null
+ )
+ )
+ }
+ }
+ }
+ }
+
+ // Get Emails
+ if (withEmails) {
+ contentResolver.query(
+ ContactsContract.CommonDataKinds.Email.CONTENT_URI,
+ arrayOf(
+ ContactsContract.CommonDataKinds.Email.CONTACT_ID,
+ ContactsContract.CommonDataKinds.Email.DATA
+ ),
+ null,
+ null,
+ null)
+ ?.use { innerCursor ->
+ while (innerCursor.moveToNext()) {
+ // This would allow you get several email addresses
+ // if the email addresses were stored in an array
+ val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
+ ?.let { map[it] }
+ ?: continue
+ innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
+ ?.let {
+ mappedContactBuilder.emails.add(
+ MappedEmail(
+ email = it,
+ matrixId = null
+ )
+ )
+ }
+ }
+ }
+ }
+ }.also { Timber.d("Took ${it}ms to fetch ${map.size} contact(s)") }
+
+ return map
+ .values
+ .filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
+ .map { it.build() }
+ }
+
+ private fun Cursor.getString(column: String): String? {
+ return getColumnIndex(column)
+ .takeIf { it != -1 }
+ ?.let { getString(it) }
+ }
+
+ private fun Cursor.getLong(column: String): Long? {
+ return getColumnIndex(column)
+ .takeIf { it != -1 }
+ ?.let { getLong(it) }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt b/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt
new file mode 100644
index 0000000000..c89a3d4b01
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.contacts
+
+import android.net.Uri
+
+class MappedContactBuilder(
+ val id: Long,
+ val displayName: String
+) {
+ var photoURI: Uri? = null
+ val msisdns = mutableListOf()
+ val emails = mutableListOf()
+
+ fun build(): MappedContact {
+ return MappedContact(
+ id = id,
+ displayName = displayName,
+ photoURI = photoURI,
+ msisdns = msisdns,
+ emails = emails
+ )
+ }
+}
+
+data class MappedContact(
+ val id: Long,
+ val displayName: String,
+ val photoURI: Uri? = null,
+ val msisdns: List = emptyList(),
+ val emails: List = emptyList()
+)
+
+data class MappedEmail(
+ val email: String,
+ val matrixId: String?
+)
+
+data class MappedMsisdn(
+ val phoneNumber: String,
+ val matrixId: String?
+)
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
index ff9865c3ea..2dc7b24ebf 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
@@ -20,8 +20,12 @@ import arrow.core.Option
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource
+import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
+import im.vector.riotx.features.notifications.PushRuleTriggerListener
+import im.vector.riotx.features.session.SessionListener
+import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton
@@ -30,23 +34,42 @@ import javax.inject.Singleton
class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
private val sessionObservableStore: ActiveSessionDataSource,
private val keyRequestHandler: KeyRequestHandler,
- private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
+ private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
+ private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
+ private val pushRuleTriggerListener: PushRuleTriggerListener,
+ private val sessionListener: SessionListener,
+ private val imageManager: ImageManager
) {
private var activeSession: AtomicReference = AtomicReference()
fun setActiveSession(session: Session) {
+ Timber.w("setActiveSession of ${session.myUserId}")
activeSession.set(session)
sessionObservableStore.post(Option.just(session))
+
keyRequestHandler.start(session)
incomingVerificationRequestHandler.start(session)
+ session.addListener(sessionListener)
+ pushRuleTriggerListener.startWithSession(session)
+ session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
+ imageManager.onSessionStarted(session)
}
fun clearActiveSession() {
+ // Do some cleanup first
+ getSafeActiveSession()?.let {
+ Timber.w("clearActiveSession of ${it.myUserId}")
+ it.callSignalingService().removeCallListener(webRtcPeerConnectionManager)
+ it.removeListener(sessionListener)
+ }
+
activeSession.set(null)
sessionObservableStore.post(Option.empty())
+
keyRequestHandler.stop()
incomingVerificationRequestHandler.stop()
+ pushRuleTriggerListener.stop()
}
fun hasActiveSession(): Boolean {
diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
index 21cff188d0..8e4f95ed54 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
@@ -23,6 +23,7 @@ import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
+import im.vector.riotx.features.contactsbook.ContactsBookFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
@@ -528,4 +529,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(WidgetFragment::class)
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
+
+ @Binds
+ @IntoMap
+ @FragmentKey(ContactsBookFragment::class)
+ fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
}
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
new file mode 100644
index 0000000000..74a01e76ec
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.di
+
+import android.content.Context
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.model.GlideUrl
+import com.github.piasy.biv.BigImageViewer
+import com.github.piasy.biv.loader.glide.GlideImageLoader
+import im.vector.matrix.android.api.session.Session
+import im.vector.riotx.ActiveSessionDataSource
+import im.vector.riotx.core.glide.FactoryUrl
+import java.io.InputStream
+import javax.inject.Inject
+
+/**
+ * This class is used to configure the library we use for images
+ */
+class ImageManager @Inject constructor(
+ private val context: Context,
+ private val activeSessionDataSource: ActiveSessionDataSource
+) {
+
+ fun onSessionStarted(session: Session) {
+ // Do this call first
+ BigImageViewer.initialize(GlideImageLoader.with(context, session.getOkHttpClient()))
+
+ val glide = Glide.get(context)
+
+ // And this one. FIXME But are losing what BigImageViewer has done to add a Progress listener
+ glide.registry.replace(GlideUrl::class.java, InputStream::class.java, FactoryUrl(activeSessionDataSource))
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
index ceb276614a..2838a42169 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
@@ -48,6 +48,7 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity
+import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity
@@ -72,6 +73,7 @@ import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.ui.UiStateRepository
import im.vector.riotx.features.widgets.WidgetActivity
import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet
+import im.vector.riotx.features.workers.signout.SignOutBottomSheetDialogFragment
@Component(
dependencies = [
@@ -135,6 +137,7 @@ interface ScreenComponent {
fun inject(activity: ReviewTermsActivity)
fun inject(activity: WidgetActivity)
fun inject(activity: VectorCallActivity)
+ fun inject(activity: VectorAttachmentViewerActivity)
/* ==========================================================================================
* BottomSheets
@@ -152,6 +155,7 @@ interface ScreenComponent {
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(bottomSheet: CallControlsBottomSheet)
+ fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
/* ==========================================================================================
* Others
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
index badfdd96c1..6ac6fa03da 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
@@ -36,7 +36,6 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
-import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module
interface ViewModelModule {
@@ -51,11 +50,6 @@ interface ViewModelModule {
* Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future.
*/
- @Binds
- @IntoMap
- @ViewModelKey(SignOutViewModel::class)
- fun bindSignOutViewModel(viewModel: SignOutViewModel): ViewModel
-
@Binds
@IntoMap
@ViewModelKey(EmojiChooserViewModel::class)
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
index e9f4dba7a5..b89da07984 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
@@ -20,6 +20,7 @@ package im.vector.riotx.core.epoxy.profiles
import android.view.View
import android.widget.ImageView
import android.widget.TextView
+import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
@@ -36,16 +37,21 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
+ @EpoxyAttribute var editable: Boolean = true
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
val bestName = matrixItem.getBestName()
- val matrixId = matrixItem.id.takeIf { it != bestName }
- holder.view.setOnClickListener(clickListener)
+ val matrixId = matrixItem.id
+ .takeIf { it != bestName }
+ // Special case for ThreePid fake matrix item
+ .takeIf { it != "@" }
+ holder.view.setOnClickListener(clickListener?.takeIf { editable })
holder.titleView.text = bestName
holder.subtitleView.setTextOrHide(matrixId)
+ holder.editableView.isVisible = editable
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
}
@@ -55,5 +61,6 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
val subtitleView by bind(R.id.matrixItemSubtitle)
val avatarImageView by bind(R.id.matrixItemAvatar)
val avatarDecorationImageView by bind(R.id.matrixItemAvatarDecoration)
+ val editableView by bind(R.id.matrixItemEditable)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
index b74f143e17..cc6eb54154 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt
@@ -23,21 +23,21 @@ import androidx.fragment.app.FragmentTransaction
import im.vector.riotx.core.platform.VectorBaseActivity
fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) {
- supportFragmentManager.commitTransactionNow { add(frameId, fragment) }
+ supportFragmentManager.commitTransaction { add(frameId, fragment) }
}
fun VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- supportFragmentManager.commitTransactionNow {
+ supportFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
- supportFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) }
+ supportFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
}
fun VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- supportFragmentManager.commitTransactionNow {
+ supportFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
index 5bd6852e8a..99a5cb5a1a 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
@@ -19,6 +19,9 @@ package im.vector.riotx.core.extensions
import android.os.Bundle
import android.util.Patterns
import androidx.fragment.app.Fragment
+import com.google.i18n.phonenumbers.NumberParseException
+import com.google.i18n.phonenumbers.PhoneNumberUtil
+import im.vector.matrix.android.api.extensions.ensurePrefix
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
@@ -33,3 +36,15 @@ fun T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bu
* Check if a CharSequence is an email
*/
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
+
+/**
+ * Check if a CharSequence is a phone number
+ */
+fun CharSequence.isMsisdn(): Boolean {
+ return try {
+ PhoneNumberUtil.getInstance().parse(ensurePrefix("+"), null)
+ true
+ } catch (e: NumberParseException) {
+ false
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
index c28dcf12d3..4c007e60d6 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
@@ -16,26 +16,32 @@
package im.vector.riotx.core.extensions
+import android.app.Activity
import android.os.Parcelable
import androidx.fragment.app.Fragment
+import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
+import im.vector.riotx.core.utils.selectTxtFileToWrite
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
- parentFragmentManager.commitTransactionNow { add(frameId, fragment) }
+ parentFragmentManager.commitTransaction { add(frameId, fragment) }
}
fun VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- parentFragmentManager.commitTransactionNow {
+ parentFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) {
- parentFragmentManager.commitTransactionNow { replace(frameId, fragment) }
+ parentFragmentManager.commitTransaction { replace(frameId, fragment) }
}
fun VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- parentFragmentManager.commitTransactionNow {
+ parentFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
@@ -51,21 +57,21 @@ fun VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragm
}
fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
- childFragmentManager.commitTransactionNow { add(frameId, fragment, tag) }
+ childFragmentManager.commitTransaction { add(frameId, fragment, tag) }
}
fun VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- childFragmentManager.commitTransactionNow {
+ childFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
- childFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) }
+ childFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
}
fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) {
- childFragmentManager.commitTransactionNow {
+ childFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
}
}
@@ -89,3 +95,27 @@ fun Fragment.getAllChildFragments(): List {
// Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0
+
+fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
+ val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
+
+ selectTxtFileToWrite(
+ activity = requireActivity(),
+ fragment = this,
+ defaultFileName = "element-megolm-export-$userId-$timestamp.txt",
+ chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
+ requestCode = requestCode
+ )
+}
+
+fun Activity.queryExportKeys(userId: String, requestCode: Int) {
+ val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
+
+ selectTxtFileToWrite(
+ activity = this,
+ fragment = null,
+ defaultFileName = "element-megolm-export-$userId-$timestamp.txt",
+ chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
+ requestCode = requestCode
+ )
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
index 987194ea2f..b9907f8789 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
@@ -38,13 +38,13 @@ inline fun > Iterable.lastMinBy(selector: (T) -> R): T?
/**
* Call each for each item, and between between each items
*/
-inline fun Collection.join(each: (T) -> Unit, between: (T) -> Unit) {
+inline fun Collection.join(each: (Int, T) -> Unit, between: (Int, T) -> Unit) {
val lastIndex = size - 1
forEachIndexed { idx, t ->
- each(t)
+ each(idx, t)
if (idx != lastIndex) {
- between(t)
+ between(idx, t)
}
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
index 29b169ffd4..9d49319896 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
@@ -24,20 +24,14 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.core.services.VectorSyncService
-import im.vector.riotx.features.notifications.PushRuleTriggerListener
-import im.vector.riotx.features.session.SessionListener
import timber.log.Timber
-fun Session.configureAndStart(context: Context,
- pushRuleTriggerListener: PushRuleTriggerListener,
- sessionListener: SessionListener) {
+fun Session.configureAndStart(context: Context) {
+ Timber.i("Configure and start session for $myUserId")
open()
- addListener(sessionListener)
setFilter(FilterService.FilterPreset.RiotFilter)
- Timber.i("Configure and start session for ${this.myUserId}")
startSyncing(context)
refreshPushers()
- pushRuleTriggerListener.startWithSession(this)
}
fun Session.startSyncing(context: Context) {
@@ -65,3 +59,12 @@ fun Session.hasUnsavedKeys(): Boolean {
return cryptoService().inboundGroupSessionsCount(false) > 0
&& cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp
}
+
+fun Session.cannotLogoutSafely(): Boolean {
+ // has some encrypted chat
+ return hasUnsavedKeys()
+ // has local cross signing keys
+ || (cryptoService().crossSigningService().allPrivateKeysKnown()
+ // That are not backed up
+ && !sharedSecretStorageService.isRecoverySetup())
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
new file mode 100644
index 0000000000..fc037894db
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.glide
+
+import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
+import com.bumptech.glide.load.model.GlideUrl
+import com.bumptech.glide.load.model.ModelLoader
+import com.bumptech.glide.load.model.ModelLoaderFactory
+import com.bumptech.glide.load.model.MultiModelLoaderFactory
+import im.vector.riotx.ActiveSessionDataSource
+import okhttp3.OkHttpClient
+import java.io.InputStream
+
+class FactoryUrl(private val activeSessionDataSource: ActiveSessionDataSource) : ModelLoaderFactory {
+
+ override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader {
+ val client = activeSessionDataSource.currentValue?.orNull()?.getOkHttpClient() ?: OkHttpClient()
+ return OkHttpUrlLoader(client)
+ }
+
+ override fun teardown() {
+ // Do nothing, this instance doesn't own the client.
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
index 191ab6d972..510eef71e1 100644
--- a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
+++ b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
@@ -65,7 +65,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
private val height: Int)
: DataFetcher {
- val client = OkHttpClient()
+ private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
override fun getDataClass(): Class {
return InputStream::class.java
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
deleted file mode 100644
index f451308c36..0000000000
--- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
+++ /dev/null
@@ -1,419 +0,0 @@
-/*
- * Copyright (C) 2011 Micah Hainline
- * Copyright (C) 2012 Triposo
- * Copyright (C) 2013 Paul Imhoff
- * Copyright (C) 2014 Shahin Yousefi
- * Copyright 2020 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-package im.vector.riotx.core.platform
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Color
-import android.text.Layout
-import android.text.Spannable
-import android.text.SpannableString
-import android.text.SpannableStringBuilder
-import android.text.Spanned
-import android.text.StaticLayout
-import android.text.TextUtils.TruncateAt
-import android.text.TextUtils.concat
-import android.text.TextUtils.copySpansFrom
-import android.text.TextUtils.indexOf
-import android.text.TextUtils.lastIndexOf
-import android.text.TextUtils.substring
-import android.text.style.ForegroundColorSpan
-import android.util.AttributeSet
-import androidx.appcompat.widget.AppCompatTextView
-import timber.log.Timber
-import java.util.ArrayList
-import java.util.regex.Pattern
-
-/*
- * Imported from https://gist.github.com/hateum/d2095575b441007d62b8
- *
- * Use it in your layout to avoid this issue: https://issuetracker.google.com/issues/121092510
- */
-
-/**
- * A [android.widget.TextView] that ellipsizes more intelligently.
- * This class supports ellipsizing multiline text through setting `android:ellipsize`
- * and `android:maxLines`.
- *
- *
- * Note: [TruncateAt.MARQUEE] ellipsizing type is not supported.
- * This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
- */
-class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = android.R.attr.textViewStyle)
- : AppCompatTextView(context, attrs, defStyle) {
-
- private val ELLIPSIS = SpannableString("\u2026")
- private val ellipsizeListeners: MutableList = ArrayList()
- private var ellipsizeStrategy: EllipsizeStrategy? = null
- var isEllipsized = false
- private set
- private var isStale = false
- private var programmaticChange = false
- private var fullText: CharSequence? = null
- private var maxLines = 0
- private var lineSpacingMult = 1.0f
- private var lineAddVertPad = 0.0f
- /**
- * The end punctuation which will be removed when appending [.ELLIPSIS].
- */
- private var mEndPunctPattern: Pattern? = null
-
- fun setEndPunctuationPattern(pattern: Pattern?) {
- mEndPunctPattern = pattern
- }
-
- fun addEllipsizeListener(listener: EllipsizeListener) {
- ellipsizeListeners.add(listener)
- }
-
- fun removeEllipsizeListener(listener: EllipsizeListener) {
- ellipsizeListeners.remove(listener)
- }
-
- /**
- * @return The maximum number of lines displayed in this [android.widget.TextView].
- */
- override fun getMaxLines(): Int {
- return maxLines
- }
-
- override fun setMaxLines(maxLines: Int) {
- super.setMaxLines(maxLines)
- this.maxLines = maxLines
- isStale = true
- }
-
- /**
- * Determines if the last fully visible line is being ellipsized.
- *
- * @return `true` if the last fully visible line is being ellipsized;
- * otherwise, returns `false`.
- */
- fun ellipsizingLastFullyVisibleLine(): Boolean {
- return maxLines == Int.MAX_VALUE
- }
-
- override fun setLineSpacing(add: Float, mult: Float) {
- lineAddVertPad = add
- lineSpacingMult = mult
- super.setLineSpacing(add, mult)
- }
-
- override fun setText(text: CharSequence?, type: BufferType) {
- if (!programmaticChange) {
- fullText = if (text is Spanned) text else text
- isStale = true
- }
- super.setText(text, type)
- }
-
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
- super.onSizeChanged(w, h, oldw, oldh)
- if (ellipsizingLastFullyVisibleLine()) {
- isStale = true
- }
- }
-
- override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
- super.setPadding(left, top, right, bottom)
- if (ellipsizingLastFullyVisibleLine()) {
- isStale = true
- }
- }
-
- override fun onDraw(canvas: Canvas) {
- if (isStale) {
- resetText()
- }
- super.onDraw(canvas)
- }
-
- /**
- * Sets the ellipsized text if appropriate.
- */
- private fun resetText() {
- val maxLines = maxLines
- var workingText = fullText
- var ellipsized = false
- if (maxLines != -1) {
- if (ellipsizeStrategy == null) setEllipsize(null)
- workingText = ellipsizeStrategy!!.processText(fullText)
- ellipsized = !ellipsizeStrategy!!.isInLayout(fullText)
- }
- if (workingText != text) {
- programmaticChange = true
- text = try {
- workingText
- } finally {
- programmaticChange = false
- }
- }
- isStale = false
- if (ellipsized != isEllipsized) {
- isEllipsized = ellipsized
- for (listener in ellipsizeListeners) {
- listener.ellipsizeStateChanged(ellipsized)
- }
- }
- }
-
- /**
- * Causes words in the text that are longer than the view is wide to be ellipsized
- * instead of broken in the middle. Use `null` to turn off ellipsizing.
- *
- *
- * Note: Method does nothing for [TruncateAt.MARQUEE]
- * ellipsizing type.
- *
- * @param where part of text to ellipsize
- */
- override fun setEllipsize(where: TruncateAt?) {
- if (where == null) {
- ellipsizeStrategy = EllipsizeNoneStrategy()
- return
- }
- ellipsizeStrategy = when (where) {
- TruncateAt.END -> EllipsizeEndStrategy()
- TruncateAt.START -> EllipsizeStartStrategy()
- TruncateAt.MIDDLE -> EllipsizeMiddleStrategy()
- TruncateAt.MARQUEE -> EllipsizeNoneStrategy()
- else -> EllipsizeNoneStrategy()
- }
- }
-
- /**
- * A listener that notifies when the ellipsize state has changed.
- */
- interface EllipsizeListener {
- fun ellipsizeStateChanged(ellipsized: Boolean)
- }
-
- /**
- * A base class for an ellipsize strategy.
- */
- private abstract inner class EllipsizeStrategy {
- /**
- * Returns ellipsized text if the text does not fit inside of the layout;
- * otherwise, returns the full text.
- *
- * @param text text to process
- * @return Ellipsized text if the text does not fit inside of the layout;
- * otherwise, returns the full text.
- */
- fun processText(text: CharSequence?): CharSequence? {
- return if (!isInLayout(text)) createEllipsizedText(text) else text
- }
-
- /**
- * Determines if the text fits inside of the layout.
- *
- * @param text text to fit
- * @return `true` if the text fits inside of the layout;
- * otherwise, returns `false`.
- */
- fun isInLayout(text: CharSequence?): Boolean {
- val layout = createWorkingLayout(text)
- return layout.lineCount <= linesCount
- }
-
- /**
- * Creates a working layout with the given text.
- *
- * @param workingText text to create layout with
- * @return [android.text.Layout] with the given text.
- */
- @Suppress("DEPRECATION")
- protected fun createWorkingLayout(workingText: CharSequence?): Layout {
- return StaticLayout(
- workingText ?: "",
- paint,
- width - compoundPaddingLeft - compoundPaddingRight,
- Layout.Alignment.ALIGN_NORMAL,
- lineSpacingMult,
- lineAddVertPad,
- false
- )
- }
-
- /**
- * Get how many lines of text we are allowed to display.
- */
- protected val linesCount: Int
- get() = if (ellipsizingLastFullyVisibleLine()) {
- val fullyVisibleLinesCount = fullyVisibleLinesCount
- if (fullyVisibleLinesCount == -1) 1 else fullyVisibleLinesCount
- } else {
- maxLines
- }
-
- /**
- * Get how many lines of text we can display so their full height is visible.
- */
- protected val fullyVisibleLinesCount: Int
- get() {
- val layout = createWorkingLayout("")
- val height = height - compoundPaddingTop - compoundPaddingBottom
- val lineHeight = layout.getLineBottom(0)
- return height / lineHeight
- }
-
- /**
- * Creates ellipsized text from the given text.
- *
- * @param fullText text to ellipsize
- * @return Ellipsized text
- */
- protected abstract fun createEllipsizedText(fullText: CharSequence?): CharSequence?
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * does not ellipsize text.
- */
- private inner class EllipsizeNoneStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- return fullText
- }
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * ellipsizes text at the end.
- */
- private inner class EllipsizeEndStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- val layout = createWorkingLayout(fullText)
- val cutOffIndex = try {
- layout.getLineEnd(maxLines - 1)
- } catch (exception: IndexOutOfBoundsException) {
- // Not sure to understand why this is happening
- Timber.e(exception, "IndexOutOfBoundsException, maxLine: $maxLines")
- 0
- }
- val textLength = fullText!!.length
- var cutOffLength = textLength - cutOffIndex
- if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
- var workingText: CharSequence = substring(fullText, 0, textLength - cutOffLength).trim()
- while (!isInLayout(concat(stripEndPunctuation(workingText), ELLIPSIS))) {
- val lastSpace = lastIndexOf(workingText, ' ')
- if (lastSpace == -1) {
- break
- }
- workingText = substring(workingText, 0, lastSpace).trim()
- }
- workingText = concat(stripEndPunctuation(workingText), ELLIPSIS)
- val dest = SpannableStringBuilder(workingText)
- if (fullText is Spanned) {
- copySpansFrom(fullText as Spanned?, 0, workingText.length, null, dest, 0)
- }
- return dest
- }
-
- /**
- * Strips the end punctuation from a given text according to [.mEndPunctPattern].
- *
- * @param workingText text to strip end punctuation from
- * @return Text without end punctuation.
- */
- fun stripEndPunctuation(workingText: CharSequence): String {
- return mEndPunctPattern!!.matcher(workingText).replaceFirst("")
- }
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * ellipsizes text at the start.
- */
- private inner class EllipsizeStartStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- val layout = createWorkingLayout(fullText)
- val cutOffIndex = layout.getLineEnd(maxLines - 1)
- val textLength = fullText!!.length
- var cutOffLength = textLength - cutOffIndex
- if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
- var workingText: CharSequence = substring(fullText, cutOffLength, textLength).trim()
- while (!isInLayout(concat(ELLIPSIS, workingText))) {
- val firstSpace = indexOf(workingText, ' ')
- if (firstSpace == -1) {
- break
- }
- workingText = substring(workingText, firstSpace, workingText.length).trim()
- }
- workingText = concat(ELLIPSIS, workingText)
- val dest = SpannableStringBuilder(workingText)
- if (fullText is Spanned) {
- copySpansFrom(fullText as Spanned?, textLength - workingText.length,
- textLength, null, dest, 0)
- }
- return dest
- }
- }
-
- /**
- * An [EllipsizingTextView.EllipsizeStrategy] that
- * ellipsizes text in the middle.
- */
- private inner class EllipsizeMiddleStrategy : EllipsizeStrategy() {
- override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
- val layout = createWorkingLayout(fullText)
- val cutOffIndex = layout.getLineEnd(maxLines - 1)
- val textLength = fullText!!.length
- var cutOffLength = textLength - cutOffIndex
- if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
- cutOffLength += cutOffIndex % 2 // Make it even.
- var firstPart = substring(
- fullText, 0, textLength / 2 - cutOffLength / 2).trim()
- var secondPart = substring(
- fullText, textLength / 2 + cutOffLength / 2, textLength).trim()
- while (!isInLayout(concat(firstPart, ELLIPSIS, secondPart))) {
- val lastSpaceFirstPart = firstPart.lastIndexOf(' ')
- val firstSpaceSecondPart = secondPart.indexOf(' ')
- if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break
- firstPart = firstPart.substring(0, lastSpaceFirstPart).trim()
- secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length).trim()
- }
- val firstDest = SpannableStringBuilder(firstPart)
- val secondDest = SpannableStringBuilder(secondPart)
- if (fullText is Spanned) {
- copySpansFrom(fullText as Spanned?, 0, firstPart.length,
- null, firstDest, 0)
- copySpansFrom(fullText as Spanned?, textLength - secondPart.length,
- textLength, null, secondDest, 0)
- }
- return concat(firstDest, ELLIPSIS, secondDest)
- }
- }
-
- companion object {
- const val ELLIPSIZE_ALPHA = 0x88
- private val DEFAULT_END_PUNCTUATION = Pattern.compile("[.!?,;:\u2026]*$", Pattern.DOTALL)
- }
-
- init {
- val a = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle, 0)
- maxLines = a.getInt(0, Int.MAX_VALUE)
- a.recycle()
- setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
- val currentTextColor = currentTextColor
- val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))
- ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
-}
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt
index b8587750a3..99c158252f 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 New Vector Ltd
+ * Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package im.vector.riotx.core.platform
import android.content.Context
import android.util.AttributeSet
+import androidx.core.content.withStyledAttributes
import androidx.core.widget.NestedScrollView
import im.vector.riotx.R
@@ -34,9 +35,9 @@ class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: Att
init {
if (attrs != null) {
- val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
- maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)
- styledAttrs.recycle()
+ context.withStyledAttributes(attrs, R.styleable.MaxHeightScrollView) {
+ maxHeight = getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
index bdd873d0cd..59bf7a8aeb 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
@@ -162,9 +162,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
return this
}
- protected fun Disposable.disposeOnDestroy(): Disposable {
+ protected fun Disposable.disposeOnDestroy() {
uiDisposables.add(this)
- return this
}
override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
index c0b1b54c09..f4343a3e58 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
@@ -234,9 +234,8 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
private val uiDisposables = CompositeDisposable()
- protected fun Disposable.disposeOnDestroyView(): Disposable {
+ protected fun Disposable.disposeOnDestroyView() {
uiDisposables.add(this)
- return this
}
/* ==========================================================================================
diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt
index d29982c9e4..455e856833 100644
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt
+++ b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt
@@ -25,6 +25,7 @@ import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
+import androidx.core.content.withStyledAttributes
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
@@ -117,16 +118,15 @@ class BottomSheetActionButton @JvmOverloads constructor(
inflate(context, R.layout.item_verification_action, this)
ButterKnife.bind(this)
- val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0)
- title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""
- subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: ""
- forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false)
- leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon)
+ context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) {
+ title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""
+ subTitle = getString(R.styleable.BottomSheetActionButton_actionDescription) ?: ""
+ forceStartPadding = getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false)
+ leftIcon = getDrawable(R.styleable.BottomSheetActionButton_leftIcon)
- rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
+ rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
- tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
-
- typedArray.recycle()
+ tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
+ }
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
index 817575d91a..0152f7c2a8 100755
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
+++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
@@ -17,16 +17,13 @@
package im.vector.riotx.core.ui.views
import android.content.Context
-import androidx.preference.PreferenceManager
import android.util.AttributeSet
import android.view.View
-import android.view.ViewGroup
-import android.widget.AbsListView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit
import androidx.core.view.isVisible
-import androidx.transition.TransitionManager
+import androidx.preference.PreferenceManager
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.OnClick
@@ -58,22 +55,12 @@ class KeysBackupBanner @JvmOverloads constructor(
var delegate: Delegate? = null
private var state: State = State.Initial
- private var scrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE
- set(value) {
- field = value
-
- val pendingV = pendingVisibility
-
- if (pendingV != null) {
- pendingVisibility = null
- visibility = pendingV
- }
- }
-
- private var pendingVisibility: Int? = null
-
init {
setupView()
+ PreferenceManager.getDefaultSharedPreferences(context).edit {
+ putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
+ putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
+ }
}
/**
@@ -91,7 +78,6 @@ class KeysBackupBanner @JvmOverloads constructor(
state = newState
hideAll()
-
when (newState) {
State.Initial -> renderInitial()
State.Hidden -> renderHidden()
@@ -102,22 +88,6 @@ class KeysBackupBanner @JvmOverloads constructor(
}
}
- override fun setVisibility(visibility: Int) {
- if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
- // Wait for scroll state to be idle
- pendingVisibility = visibility
- return
- }
-
- if (visibility != getVisibility()) {
- // Schedule animation
- val parent = parent as ViewGroup
- TransitionManager.beginDelayedTransition(parent)
- }
-
- super.setVisibility(visibility)
- }
-
override fun onClick(v: View?) {
when (state) {
is State.Setup -> {
@@ -166,6 +136,8 @@ class KeysBackupBanner @JvmOverloads constructor(
ButterKnife.bind(this)
setOnClickListener(this)
+ textView1.setOnClickListener(this)
+ textView2.setOnClickListener(this)
}
private fun renderInitial() {
@@ -184,9 +156,9 @@ class KeysBackupBanner @JvmOverloads constructor(
} else {
isVisible = true
- textView1.setText(R.string.keys_backup_banner_setup_line1)
+ textView1.setText(R.string.secure_backup_banner_setup_line1)
textView2.isVisible = true
- textView2.setText(R.string.keys_backup_banner_setup_line2)
+ textView2.setText(R.string.secure_backup_banner_setup_line2)
close.isVisible = true
}
}
@@ -218,10 +190,10 @@ class KeysBackupBanner @JvmOverloads constructor(
}
private fun renderBackingUp() {
- // Do not render when backing up anymore
- isVisible = false
-
- textView1.setText(R.string.keys_backup_banner_in_progress)
+ isVisible = true
+ textView1.setText(R.string.secure_backup_banner_setup_line1)
+ textView2.isVisible = true
+ textView2.setText(R.string.keys_backup_banner_in_progress)
loading.isVisible = true
}
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
index 4c4a553e5c..6f6057cb43 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
@@ -36,6 +36,9 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD
private val behaviorRelay = createRelay()
+ val currentValue: T?
+ get() = behaviorRelay.value
+
override fun observe(): Observable {
return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread())
}
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
index 2520f44f50..9c2d12514a 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
@@ -424,6 +424,33 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
}
}
+/**
+ * Ask the user to select a location and a file name to write in
+ */
+fun selectTxtFileToWrite(
+ activity: Activity,
+ fragment: Fragment?,
+ defaultFileName: String,
+ chooserHint: String,
+ requestCode: Int
+) {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = "text/plain"
+ intent.putExtra(Intent.EXTRA_TITLE, defaultFileName)
+
+ try {
+ val chooserIntent = Intent.createChooser(intent, chooserHint)
+ if (fragment != null) {
+ fragment.startActivityForResult(chooserIntent, requestCode)
+ } else {
+ activity.startActivityForResult(chooserIntent, requestCode)
+ }
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ activity.toast(R.string.error_no_external_application_found)
+ }
+}
+
// ==============================================================================================================
// Media utils
// ==============================================================================================================
diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
index 4790b26ad0..6f081d52de 100644
--- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
+++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
@@ -63,12 +63,12 @@ const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA = 569
const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA = 570
const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571
const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
-const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
+const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579
/**
* Log the used permissions statuses.
diff --git a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
index 05f14ae4f2..070375d201 100644
--- a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
@@ -22,6 +22,7 @@ import android.os.Build
import androidx.annotation.RequiresApi
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.tryThis
+import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.call.CallState
import im.vector.matrix.android.api.session.call.CallsListener
import im.vector.matrix.android.api.session.call.EglUtils
@@ -31,7 +32,7 @@ import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
-import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.core.services.BluetoothHeadsetReceiver
import im.vector.riotx.core.services.CallService
import im.vector.riotx.core.services.WiredHeadsetStateReceiver
@@ -71,9 +72,12 @@ import javax.inject.Singleton
@Singleton
class WebRtcPeerConnectionManager @Inject constructor(
private val context: Context,
- private val sessionHolder: ActiveSessionHolder
+ private val activeSessionDataSource: ActiveSessionDataSource
) : CallsListener {
+ private val currentSession: Session?
+ get() = activeSessionDataSource.currentValue?.orNull()
+
interface CurrentCallListener {
fun onCurrentCallChange(call: MxCall?)
fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {}
@@ -288,15 +292,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
private fun getTurnServer(callback: ((TurnServerResponse?) -> Unit)) {
- sessionHolder.getActiveSession().callSignalingService().getTurnServer(object : MatrixCallback {
- override fun onSuccess(data: TurnServerResponse?) {
- callback(data)
- }
+ currentSession?.callSignalingService()
+ ?.getTurnServer(object : MatrixCallback {
+ override fun onSuccess(data: TurnServerResponse?) {
+ callback(data)
+ }
- override fun onFailure(failure: Throwable) {
- callback(null)
- }
- })
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ })
}
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
@@ -310,7 +315,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCall?.mxCall
?.takeIf { it.state is CallState.Connected }
?.let { mxCall ->
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.roomId
// Start background service with notification
CallService.onPendingCall(
@@ -318,7 +323,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId)
}
@@ -373,14 +378,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
val mxCall = callContext.mxCall
// Update service state
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.roomId
CallService.onPendingCall(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
executor.execute {
@@ -563,14 +568,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
?.let { mxCall ->
// Start background service with notification
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId
CallService.onOnGoingCallBackground(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
}
@@ -631,20 +636,20 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
- val createdCall = sessionHolder.getSafeActiveSession()?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
+ val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
val callContext = CallContext(createdCall)
audioManager.startForCall(createdCall)
currentCall = callContext
- val name = sessionHolder.getSafeActiveSession()?.getUser(createdCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
?: createdCall.otherUserId
CallService.onOutgoingCallRinging(
context = context.applicationContext,
isVideo = createdCall.isVideoCall,
roomName = name,
roomId = createdCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = createdCall.callId)
executor.execute {
@@ -693,14 +698,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
// Start background service with notification
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId
CallService.onIncomingCallRinging(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
@@ -818,14 +823,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
val mxCall = call.mxCall
// Update service state
- val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
+ val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId
CallService.onPendingCall(
context = context,
isVideo = mxCall.isVideoCall,
roomName = name,
roomId = mxCall.roomId,
- matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
+ matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId
)
executor.execute {
diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
index 7c32a34aff..2b38a1ac25 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
@@ -17,6 +17,9 @@
package im.vector.riotx.features.command
import im.vector.matrix.android.api.MatrixPatterns
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.riotx.core.extensions.isEmail
+import im.vector.riotx.core.extensions.isMsisdn
import timber.log.Timber
object CommandParser {
@@ -139,15 +142,24 @@ object CommandParser {
if (messageParts.size >= 2) {
val userId = messageParts[1]
- if (MatrixPatterns.isUserId(userId)) {
- ParsedCommand.Invite(
- userId,
- textMessage.substring(Command.INVITE.length + userId.length)
- .trim()
- .takeIf { it.isNotBlank() }
- )
- } else {
- ParsedCommand.ErrorSyntax(Command.INVITE)
+ when {
+ MatrixPatterns.isUserId(userId) -> {
+ ParsedCommand.Invite(
+ userId,
+ textMessage.substring(Command.INVITE.length + userId.length)
+ .trim()
+ .takeIf { it.isNotBlank() }
+ )
+ }
+ userId.isEmail() -> {
+ ParsedCommand.Invite3Pid(ThreePid.Email(userId))
+ }
+ userId.isMsisdn() -> {
+ ParsedCommand.Invite3Pid(ThreePid.Msisdn(userId))
+ }
+ else -> {
+ ParsedCommand.ErrorSyntax(Command.INVITE)
+ }
}
} else {
ParsedCommand.ErrorSyntax(Command.INVITE)
diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
index 44ad2265e1..041da3dcac 100644
--- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
+++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
@@ -16,6 +16,8 @@
package im.vector.riotx.features.command
+import im.vector.matrix.android.api.session.identity.ThreePid
+
/**
* Represent a parsed command
*/
@@ -41,6 +43,7 @@ sealed class ParsedCommand {
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand()
class Invite(val userId: String, val reason: String?) : ParsedCommand()
+ class Invite3Pid(val threePid: ThreePid) : ParsedCommand()
class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class ChangeTopic(val topic: String) : ParsedCommand()
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt
new file mode 100644
index 0000000000..8615838571
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.contactsbook
+
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.epoxy.ClickListener
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.epoxy.onClick
+import im.vector.riotx.core.extensions.setTextOrHide
+
+@EpoxyModelClass(layout = R.layout.item_contact_detail)
+abstract class ContactDetailItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute lateinit var threePid: String
+ @EpoxyAttribute var matrixId: String? = null
+ @EpoxyAttribute var clickListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.view.onClick(clickListener)
+ holder.nameView.text = threePid
+ holder.matrixIdView.setTextOrHide(matrixId)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val nameView by bind(R.id.contactDetailName)
+ val matrixIdView by bind(R.id.contactDetailMatrixId)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt
new file mode 100644
index 0000000000..9a6bf8f144
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.contactsbook
+
+import android.widget.ImageView
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.contacts.MappedContact
+import im.vector.riotx.core.epoxy.VectorEpoxyHolder
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.features.home.AvatarRenderer
+
+@EpoxyModelClass(layout = R.layout.item_contact_main)
+abstract class ContactItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
+ @EpoxyAttribute lateinit var mappedContact: MappedContact
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ // If name is empty, use userId as name and force it being centered
+ holder.nameView.text = mappedContact.displayName
+ avatarRenderer.render(mappedContact, holder.avatarImageView)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val nameView by bind(R.id.contactDisplayName)
+ val avatarImageView by bind(R.id.contactAvatar)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt
new file mode 100644
index 0000000000..001630d398
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.contactsbook
+
+import im.vector.riotx.core.platform.VectorViewModelAction
+
+sealed class ContactsBookAction : VectorViewModelAction {
+ data class FilterWith(val filter: String) : ContactsBookAction()
+ data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt
new file mode 100644
index 0000000000..796ed0d80c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.contactsbook
+
+import com.airbnb.epoxy.EpoxyController
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.riotx.R
+import im.vector.riotx.core.contacts.MappedContact
+import im.vector.riotx.core.epoxy.errorWithRetryItem
+import im.vector.riotx.core.epoxy.loadingItem
+import im.vector.riotx.core.epoxy.noResultItem
+import im.vector.riotx.core.error.ErrorFormatter
+import im.vector.riotx.core.resources.StringProvider
+import im.vector.riotx.features.home.AvatarRenderer
+import javax.inject.Inject
+
+class ContactsBookController @Inject constructor(
+ private val stringProvider: StringProvider,
+ private val avatarRenderer: AvatarRenderer,
+ private val errorFormatter: ErrorFormatter) : EpoxyController() {
+
+ private var state: ContactsBookViewState? = null
+
+ var callback: Callback? = null
+
+ init {
+ requestModelBuild()
+ }
+
+ fun setData(state: ContactsBookViewState) {
+ this.state = state
+ requestModelBuild()
+ }
+
+ override fun buildModels() {
+ val currentState = state ?: return
+ val hasSearch = currentState.searchTerm.isNotEmpty()
+ when (val asyncMappedContacts = currentState.mappedContacts) {
+ is Uninitialized -> renderEmptyState(false)
+ is Loading -> renderLoading()
+ is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
+ is Fail -> renderFailure(asyncMappedContacts.error)
+ }
+ }
+
+ private fun renderLoading() {
+ loadingItem {
+ id("loading")
+ loadingText(stringProvider.getString(R.string.loading_contact_book))
+ }
+ }
+
+ private fun renderFailure(failure: Throwable) {
+ errorWithRetryItem {
+ id("error")
+ text(errorFormatter.toHumanReadable(failure))
+ }
+ }
+
+ private fun renderSuccess(mappedContacts: List,
+ hasSearch: Boolean,
+ onlyBoundContacts: Boolean) {
+ if (mappedContacts.isEmpty()) {
+ renderEmptyState(hasSearch)
+ } else {
+ renderContacts(mappedContacts, onlyBoundContacts)
+ }
+ }
+
+ private fun renderContacts(mappedContacts: List, onlyBoundContacts: Boolean) {
+ for (mappedContact in mappedContacts) {
+ contactItem {
+ id(mappedContact.id)
+ mappedContact(mappedContact)
+ avatarRenderer(avatarRenderer)
+ }
+ mappedContact.emails
+ .forEachIndexed { index, it ->
+ if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
+
+ contactDetailItem {
+ id("${mappedContact.id}-e-$index-${it.email}")
+ threePid(it.email)
+ matrixId(it.matrixId)
+ clickListener {
+ if (it.matrixId != null) {
+ callback?.onMatrixIdClick(it.matrixId)
+ } else {
+ callback?.onThreePidClick(ThreePid.Email(it.email))
+ }
+ }
+ }
+ }
+ mappedContact.msisdns
+ .forEachIndexed { index, it ->
+ if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
+
+ contactDetailItem {
+ id("${mappedContact.id}-m-$index-${it.phoneNumber}")
+ threePid(it.phoneNumber)
+ matrixId(it.matrixId)
+ clickListener {
+ if (it.matrixId != null) {
+ callback?.onMatrixIdClick(it.matrixId)
+ } else {
+ callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun renderEmptyState(hasSearch: Boolean) {
+ val noResultRes = if (hasSearch) {
+ R.string.no_result_placeholder
+ } else {
+ R.string.empty_contact_book
+ }
+ noResultItem {
+ id("noResult")
+ text(stringProvider.getString(noResultRes))
+ }
+ }
+
+ interface Callback {
+ fun onMatrixIdClick(matrixId: String)
+ fun onThreePidClick(threePid: ThreePid)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt
new file mode 100644
index 0000000000..2a2fd9fb5d
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.contactsbook
+
+import android.os.Bundle
+import android.view.View
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.activityViewModel
+import com.airbnb.mvrx.withState
+import com.jakewharton.rxbinding3.widget.checkedChanges
+import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.matrix.android.api.session.user.model.User
+import im.vector.riotx.R
+import im.vector.riotx.core.extensions.cleanup
+import im.vector.riotx.core.extensions.configureWith
+import im.vector.riotx.core.extensions.hideKeyboard
+import im.vector.riotx.core.platform.VectorBaseFragment
+import im.vector.riotx.features.userdirectory.PendingInvitee
+import im.vector.riotx.features.userdirectory.UserDirectoryAction
+import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
+import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
+import im.vector.riotx.features.userdirectory.UserDirectoryViewModel
+import kotlinx.android.synthetic.main.fragment_contacts_book.*
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+class ContactsBookFragment @Inject constructor(
+ val contactsBookViewModelFactory: ContactsBookViewModel.Factory,
+ private val contactsBookController: ContactsBookController
+) : VectorBaseFragment(), ContactsBookController.Callback {
+
+ override fun getLayoutResId() = R.layout.fragment_contacts_book
+ private val viewModel: UserDirectoryViewModel by activityViewModel()
+
+ // Use activityViewModel to avoid loading several times the data
+ private val contactsBookViewModel: ContactsBookViewModel by activityViewModel()
+
+ private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
+ setupRecyclerView()
+ setupFilterView()
+ setupOnlyBoundContactsView()
+ setupCloseView()
+ }
+
+ private fun setupOnlyBoundContactsView() {
+ phoneBookOnlyBoundContacts.checkedChanges()
+ .subscribe {
+ contactsBookViewModel.handle(ContactsBookAction.OnlyBoundContacts(it))
+ }
+ .disposeOnDestroyView()
+ }
+
+ private fun setupFilterView() {
+ phoneBookFilter
+ .textChanges()
+ .skipInitialValue()
+ .debounce(300, TimeUnit.MILLISECONDS)
+ .subscribe {
+ contactsBookViewModel.handle(ContactsBookAction.FilterWith(it.toString()))
+ }
+ .disposeOnDestroyView()
+ }
+
+ override fun onDestroyView() {
+ phoneBookRecyclerView.cleanup()
+ contactsBookController.callback = null
+ super.onDestroyView()
+ }
+
+ private fun setupRecyclerView() {
+ contactsBookController.callback = this
+ phoneBookRecyclerView.configureWith(contactsBookController)
+ }
+
+ private fun setupCloseView() {
+ phoneBookClose.debouncedClicks {
+ sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
+ }
+ }
+
+ override fun invalidate() = withState(contactsBookViewModel) { state ->
+ phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
+ contactsBookController.setData(state)
+ }
+
+ override fun onMatrixIdClick(matrixId: String) {
+ view?.hideKeyboard()
+ viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
+ sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
+ }
+
+ override fun onThreePidClick(threePid: ThreePid) {
+ view?.hideKeyboard()
+ viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
+ sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
new file mode 100644
index 0000000000..3eb6b165b8
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.contactsbook
+
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.viewModelScope
+import com.airbnb.mvrx.ActivityViewModelContext
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.ViewModelContext
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.identity.FoundThreePid
+import im.vector.matrix.android.api.session.identity.ThreePid
+import im.vector.riotx.core.contacts.ContactsDataSource
+import im.vector.riotx.core.contacts.MappedContact
+import im.vector.riotx.core.extensions.exhaustive
+import im.vector.riotx.core.platform.EmptyViewEvents
+import im.vector.riotx.core.platform.VectorViewModel
+import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
+import im.vector.riotx.features.invite.InviteUsersToRoomActivity
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+private typealias PhoneBookSearch = String
+
+class ContactsBookViewModel @AssistedInject constructor(@Assisted
+ initialState: ContactsBookViewState,
+ private val contactsDataSource: ContactsDataSource,
+ private val session: Session)
+ : VectorViewModel(initialState) {
+
+ @AssistedInject.Factory
+ interface Factory {
+ fun create(initialState: ContactsBookViewState): ContactsBookViewModel
+ }
+
+ companion object : MvRxViewModelFactory {
+
+ override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? {
+ return when (viewModelContext) {
+ is FragmentViewModelContext -> (viewModelContext.fragment() as ContactsBookFragment).contactsBookViewModelFactory.create(state)
+ is ActivityViewModelContext -> {
+ when (viewModelContext.activity()) {
+ is CreateDirectRoomActivity -> viewModelContext.activity().contactsBookViewModelFactory.create(state)
+ is InviteUsersToRoomActivity -> viewModelContext.activity().contactsBookViewModelFactory.create(state)
+ else -> error("Wrong activity or fragment")
+ }
+ }
+ else -> error("Wrong activity or fragment")
+ }
+ }
+ }
+
+ private var allContacts: List = emptyList()
+ private var mappedContacts: List = emptyList()
+
+ init {
+ loadContacts()
+
+ selectSubscribe(ContactsBookViewState::searchTerm, ContactsBookViewState::onlyBoundContacts) { _, _ ->
+ updateFilteredMappedContacts()
+ }
+ }
+
+ private fun loadContacts() {
+ setState {
+ copy(
+ mappedContacts = Loading()
+ )
+ }
+
+ viewModelScope.launch(Dispatchers.IO) {
+ allContacts = contactsDataSource.getContacts(
+ withEmails = true,
+ // Do not handle phone numbers for the moment
+ withMsisdn = false
+ )
+ mappedContacts = allContacts
+
+ setState {
+ copy(
+ mappedContacts = Success(allContacts)
+ )
+ }
+
+ performLookup(allContacts)
+ updateFilteredMappedContacts()
+ }
+ }
+
+ private fun performLookup(data: List) {
+ viewModelScope.launch {
+ val threePids = data.flatMap { contact ->
+ contact.emails.map { ThreePid.Email(it.email) } +
+ contact.msisdns.map { ThreePid.Msisdn(it.phoneNumber) }
+ }
+ session.identityService().lookUp(threePids, object : MatrixCallback> {
+ override fun onFailure(failure: Throwable) {
+ // Ignore
+ Timber.w(failure, "Unable to perform the lookup")
+ }
+
+ override fun onSuccess(data: List) {
+ mappedContacts = allContacts.map { contactModel ->
+ contactModel.copy(
+ emails = contactModel.emails.map { email ->
+ email.copy(
+ matrixId = data
+ .firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email }
+ ?.matrixId
+ )
+ },
+ msisdns = contactModel.msisdns.map { msisdn ->
+ msisdn.copy(
+ matrixId = data
+ .firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber }
+ ?.matrixId
+ )
+ }
+ )
+ }
+
+ setState {
+ copy(
+ isBoundRetrieved = true
+ )
+ }
+
+ updateFilteredMappedContacts()
+ }
+ })
+ }
+ }
+
+ private fun updateFilteredMappedContacts() = withState { state ->
+ val filteredMappedContacts = mappedContacts
+ .filter { it.displayName.contains(state.searchTerm, true) }
+ .filter { contactModel ->
+ !state.onlyBoundContacts
+ || contactModel.emails.any { it.matrixId != null } || contactModel.msisdns.any { it.matrixId != null }
+ }
+
+ setState {
+ copy(
+ filteredMappedContacts = filteredMappedContacts
+ )
+ }
+ }
+
+ override fun handle(action: ContactsBookAction) {
+ when (action) {
+ is ContactsBookAction.FilterWith -> handleFilterWith(action)
+ is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
+ }.exhaustive
+ }
+
+ private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
+ setState {
+ copy(
+ onlyBoundContacts = action.onlyBoundContacts
+ )
+ }
+ }
+
+ private fun handleFilterWith(action: ContactsBookAction.FilterWith) {
+ setState {
+ copy(
+ searchTerm = action.filter
+ )
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewState.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewState.kt
new file mode 100644
index 0000000000..8f59403d6a
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewState.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.contactsbook
+
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MvRxState
+import im.vector.riotx.core.contacts.MappedContact
+
+data class ContactsBookViewState(
+ // All the contacts on the phone
+ val mappedContacts: Async> = Loading(),
+ // Use to filter contacts by display name
+ val searchTerm: String = "",
+ // Tru to display only bound contacts with their bound 2pid
+ val onlyBoundContacts: Boolean = false,
+ // All contacts, filtered by searchTerm and onlyBoundContacts
+ val filteredMappedContacts: List = emptyList(),
+ // True when the identity service has return some data
+ val isBoundRetrieved: Boolean = false
+) : MvRxState
diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
index f995f82ff7..fad36cc281 100644
--- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
@@ -16,9 +16,9 @@
package im.vector.riotx.features.createdirect
-import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.core.platform.VectorViewModelAction
+import im.vector.riotx.features.userdirectory.PendingInvitee
sealed class CreateDirectRoomAction : VectorViewModelAction {
- data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set) : CreateDirectRoomAction()
+ data class CreateRoomAndInviteSelectedUsers(val invitees: Set) : CreateDirectRoomAction()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
index ef3e9bdeff..72244d1c94 100644
--- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
@@ -35,8 +35,15 @@ import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack
+import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData
+import im.vector.riotx.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
+import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
+import im.vector.riotx.core.utils.allGranted
+import im.vector.riotx.core.utils.checkPermissions
+import im.vector.riotx.features.contactsbook.ContactsBookFragment
+import im.vector.riotx.features.contactsbook.ContactsBookViewModel
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
@@ -53,6 +60,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
+ @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
@@ -68,12 +76,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
- UserDirectorySharedAction.OpenUsersDirectory ->
+ UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
- UserDirectorySharedAction.Close -> finish()
- UserDirectorySharedAction.GoBack -> onBackPressed()
+ UserDirectorySharedAction.Close -> finish()
+ UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
- }
+ UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
+ }.exhaustive
}
.disposeOnDestroy()
if (isFirstCreation()) {
@@ -91,9 +100,27 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
}
}
+ private fun openPhoneBook() {
+ // Check permission first
+ if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
+ this,
+ PERMISSION_REQUEST_CODE_READ_CONTACTS,
+ 0)) {
+ addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
+ }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ if (allGranted(grantResults)) {
+ if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
+ addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
+ }
+ }
+ }
+
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_create_direct_room) {
- viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers))
+ viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.invitees))
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
index 1800759da6..319671b230 100644
--- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
@@ -23,9 +23,10 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
-import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.rx.rx
+import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
+import im.vector.riotx.features.userdirectory.PendingInvitee
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
@@ -48,16 +49,22 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
override fun handle(action: CreateDirectRoomAction) {
when (action) {
- is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers)
+ is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.invitees)
}
}
- private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) {
- val roomParams = CreateRoomParams(
- invitedUserIds = selectedUsers.map { it.userId }
- )
- .setDirectMessage()
- .enableEncryptionIfInvitedUsersSupportIt()
+ private fun createRoomAndInviteSelectedUsers(invitees: Set) {
+ val roomParams = CreateRoomParams()
+ .apply {
+ invitees.forEach {
+ when (it) {
+ is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId)
+ is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
+ }.exhaustive
+ }
+ setDirectMessage()
+ enableEncryptionIfInvitedUsersSupportIt = true
+ }
session.rx()
.createRoom(roomParams)
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
index dc4199e9f5..2467334f69 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
@@ -17,37 +17,34 @@
package im.vector.riotx.features.crypto.keys
import android.content.Context
-import android.os.Environment
+import android.net.Uri
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback
-import im.vector.riotx.core.files.addEntryToDownloadManager
-import im.vector.riotx.core.files.writeToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import java.io.File
class KeysExporter(private val session: Session) {
/**
* Export keys and return the file path with the callback
*/
- fun export(context: Context, password: String, callback: MatrixCallback) {
+ fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback) {
GlobalScope.launch(Dispatchers.Main) {
runCatching {
- val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) }
withContext(Dispatchers.IO) {
- val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
- val file = File(parentDir, "element-keys-" + System.currentTimeMillis() + ".txt")
-
- writeToFile(data, file)
-
- addEntryToDownloadManager(context, file, "text/plain")
-
- file.absolutePath
+ val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) }
+ val os = context.contentResolver?.openOutputStream(uri)
+ if (os == null) {
+ false
+ } else {
+ os.write(data)
+ os.flush()
+ true
+ }
}
}.foldToCallback(callback)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
index faada7ba3e..2faff3d112 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
@@ -49,7 +49,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
val recoveryKey = recoveryCode.value!!
try {
- sharedViewModel.recoverUsingBackupPass(recoveryKey)
+ sharedViewModel.recoverUsingBackupRecoveryKey(recoveryKey)
} catch (failure: Throwable) {
recoveryCodeErrorText.postValue(stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt))
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
index c7d3da30ea..f42fee0030 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
@@ -15,6 +15,7 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
+import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
@@ -25,12 +26,9 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.extensions.observeEvent
+import im.vector.riotx.core.extensions.queryExportKeys
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.SimpleFragmentActivity
-import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
-import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
-import im.vector.riotx.core.utils.allGranted
-import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter
@@ -95,7 +93,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
.show()
}
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
- exportKeysManually()
+ queryExportKeys(session.myUserId, REQUEST_CODE_SAVE_MEGOLM_EXPORT)
}
}
}
@@ -127,50 +125,45 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
})
}
- private fun exportKeysManually() {
- if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
- this,
- PERMISSION_REQUEST_CODE_EXPORT_KEYS,
- R.string.permissions_rationale_msg_keys_backup_export)) {
- ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
- override fun onPassphrase(passphrase: String) {
- showWaitingView()
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
+ val uri = data?.data
+ if (resultCode == Activity.RESULT_OK && uri != null) {
+ ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
+ override fun onPassphrase(passphrase: String) {
+ showWaitingView()
- KeysExporter(session)
- .export(this@KeysBackupSetupActivity,
- passphrase,
- object : MatrixCallback {
- override fun onSuccess(data: String) {
- hideWaitingView()
-
- AlertDialog.Builder(this@KeysBackupSetupActivity)
- .setMessage(getString(R.string.encryption_export_saved_as, data))
- .setCancelable(false)
- .setPositiveButton(R.string.ok) { _, _ ->
- val resultIntent = Intent()
- resultIntent.putExtra(MANUAL_EXPORT, true)
- setResult(RESULT_OK, resultIntent)
+ KeysExporter(session)
+ .export(this@KeysBackupSetupActivity,
+ passphrase,
+ uri,
+ object : MatrixCallback {
+ override fun onSuccess(data: Boolean) {
+ if (data) {
+ toast(getString(R.string.encryption_exported_successfully))
+ Intent().apply {
+ putExtra(MANUAL_EXPORT, true)
+ }.let {
+ setResult(Activity.RESULT_OK, it)
finish()
}
- .show()
- }
+ }
+ hideWaitingView()
+ }
- override fun onFailure(failure: Throwable) {
- toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
- hideWaitingView()
- }
- })
- }
- })
- }
- }
-
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- if (allGranted(grantResults)) {
- if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
- exportKeysManually()
+ override fun onFailure(failure: Throwable) {
+ toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
+ hideWaitingView()
+ }
+ })
+ }
+ })
+ } else {
+ toast(getString(R.string.unexpected_error))
+ hideWaitingView()
}
}
+ super.onActivityResult(requestCode, resultCode, data)
}
override fun onBackPressed() {
@@ -205,6 +198,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
const val KEYS_VERSION = "KEYS_VERSION"
const val MANUAL_EXPORT = "MANUAL_EXPORT"
const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT"
+ const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 101
fun intent(context: Context, showManualExport: Boolean): Intent {
val intent = Intent(context, KeysBackupSetupActivity::class.java)
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
index d9a90eb457..6381786e57 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
@@ -48,6 +48,9 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
lateinit var session: Session
+ val userId: String
+ get() = session.myUserId
+
var showManualExport: MutableLiveData = MutableLiveData()
var navigateEvent: MutableLiveData> = MutableLiveData()
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt
index e6cf53206f..95fe9b0ffa 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt
@@ -15,13 +15,13 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
-import android.os.AsyncTask
import android.os.Bundle
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import androidx.lifecycle.Observer
+import androidx.lifecycle.viewModelScope
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.OnClick
@@ -33,6 +33,8 @@ import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.PasswordStrengthBar
import im.vector.riotx.features.settings.VectorLocale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import javax.inject.Inject
class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() {
@@ -117,9 +119,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment()
if (newValue.isEmpty()) {
viewModel.passwordStrength.value = null
} else {
- AsyncTask.execute {
+ viewModel.viewModelScope.launch(Dispatchers.IO) {
val strength = zxcvbn.measure(newValue)
- activity?.runOnUiThread {
+ launch(Dispatchers.Main) {
viewModel.passwordStrength.value = strength
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
index 21a25f1684..28711115c3 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
@@ -15,8 +15,10 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
import android.os.Bundle
-import android.os.Environment
import android.view.View
import android.widget.Button
import android.widget.TextView
@@ -29,25 +31,27 @@ import butterknife.BindView
import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotx.R
-import im.vector.riotx.core.files.addEntryToDownloadManager
-import im.vector.riotx.core.files.writeToFile
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.LiveEvent
-import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
-import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
-import im.vector.riotx.core.utils.allGranted
-import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
+import im.vector.riotx.core.utils.selectTxtFileToWrite
import im.vector.riotx.core.utils.startSharePlainTextIntent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import java.io.File
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
import javax.inject.Inject
class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() {
+ companion object {
+ private const val SAVE_RECOVERY_KEY_REQUEST_CODE = 2754
+ }
+
override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3
@BindView(R.id.keys_backup_setup_step3_button)
@@ -130,15 +134,15 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
dialog.findViewById(R.id.keys_backup_setup_save)?.setOnClickListener {
- val permissionsChecked = checkPermissions(
- PERMISSIONS_FOR_WRITING_FILES,
- this,
- PERMISSION_REQUEST_CODE_EXPORT_KEYS,
- R.string.permissions_rationale_msg_keys_backup_export
+ val userId = viewModel.userId
+ val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
+ selectTxtFileToWrite(
+ activity = requireActivity(),
+ fragment = this,
+ defaultFileName = "recovery-key-$userId-$timestamp.txt",
+ chooserHint = getString(R.string.save_recovery_key_chooser_hint),
+ requestCode = SAVE_RECOVERY_KEY_REQUEST_CODE
)
- if (permissionsChecked) {
- exportRecoveryKeyToFile(recoveryKey)
- }
dialog.dismiss()
}
@@ -163,34 +167,32 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
}
- private fun exportRecoveryKeyToFile(data: String) {
+ private fun exportRecoveryKeyToFile(uri: Uri, data: String) {
GlobalScope.launch(Dispatchers.Main) {
Try {
withContext(Dispatchers.IO) {
- val parentDir = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
- val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
-
- writeToFile(data, file)
-
- addEntryToDownloadManager(requireContext(), file, "text/plain")
-
- file.absolutePath
+ requireContext().contentResolver.openOutputStream(uri)
+ ?.use { os ->
+ os.write(data.toByteArray())
+ os.flush()
+ }
}
+ ?: throw IOException("Unable to write the file")
}
.fold(
{ throwable ->
- context?.let {
+ activity?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
- .setMessage(throwable.localizedMessage)
+ .setMessage(errorFormatter.toHumanReadable(throwable))
}
},
- { path ->
+ {
viewModel.copyHasBeenMade = true
-
- context?.let {
+ activity?.let {
AlertDialog.Builder(it)
- .setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
+ .setTitle(R.string.dialog_title_success)
+ .setMessage(R.string.recovery_key_export_saved)
}
}
)
@@ -200,11 +202,14 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
}
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- if (allGranted(grantResults)) {
- if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
- viewModel.recoveryKey.value?.let {
- exportRecoveryKeyToFile(it)
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ when (requestCode) {
+ SAVE_RECOVERY_KEY_REQUEST_CODE -> {
+ val uri = data?.data
+ if (resultCode == Activity.RESULT_OK && uri != null) {
+ viewModel.recoveryKey.value?.let {
+ exportRecoveryKeyToFile(uri, it)
+ }
}
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt
index f14d27b3d9..945a8c2866 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt
@@ -45,7 +45,8 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class Args(
- val initCrossSigningOnly: Boolean
+ val initCrossSigningOnly: Boolean,
+ val forceReset4S: Boolean
) : Parcelable
override val showExpanded = true
@@ -180,10 +181,15 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
const val EXTRA_ARGS = "EXTRA_ARGS"
- fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean) {
+ fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean, forceReset4S: Boolean) {
BootstrapBottomSheet().apply {
isCancelable = false
- arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(initCrossSigningOnly)) }
+ arguments = Bundle().apply {
+ this.putParcelable(EXTRA_ARGS, Args(
+ initCrossSigningOnly,
+ forceReset4S
+ ))
+ }
}.show(fragmentManager, "BootstrapBottomSheet")
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
index 6a3fadbcb3..8781cbe570 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
@@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
+import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.util.awaitCallback
@@ -84,8 +85,10 @@ class BootstrapCrossSigningTask @Inject constructor(
override suspend fun execute(params: Params): BootstrapResult {
val crossSigningService = session.cryptoService().crossSigningService()
+ Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...")
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
if (!crossSigningService.isCrossSigningInitialized()) {
+ Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize")
params.progressListener?.onProgress(
WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
@@ -104,8 +107,9 @@ class BootstrapCrossSigningTask @Inject constructor(
return handleInitializeXSigningError(failure)
}
} else {
- // not sure how this can happen??
+ Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup")
if (params.initOnlyCrossSigning) {
+ // not sure how this can happen??
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
}
}
@@ -119,6 +123,8 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2),
isIndeterminate = true)
)
+
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}")
try {
keyInfo = awaitCallback {
params.passphrase?.let { passphrase ->
@@ -141,6 +147,7 @@ class BootstrapCrossSigningTask @Inject constructor(
}
}
} catch (failure: Failure) {
+ Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>")
return BootstrapResult.FailedToCreateSSSSKey(failure)
}
@@ -149,19 +156,24 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key),
isIndeterminate = true)
)
+
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key")
try {
awaitCallback {
ssssService.setDefaultKey(keyInfo.keyId, it)
}
} catch (failure: Failure) {
// Maybe we could just ignore this error?
+ Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>")
return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
}
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys")
val xKeys = crossSigningService.getCrossSigningPrivateKeys()
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey
val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey
val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys success")
try {
params.progressListener?.onProgress(
@@ -170,6 +182,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true
)
)
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...")
awaitCallback {
ssssService.storeSecret(
MASTER_KEY_SSSS_NAME,
@@ -183,6 +196,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true
)
)
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...")
awaitCallback {
ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME,
@@ -196,6 +210,7 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true
)
)
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...")
awaitCallback {
ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME,
@@ -204,6 +219,7 @@ class BootstrapCrossSigningTask @Inject constructor(
)
}
} catch (failure: Failure) {
+ Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>")
// Maybe we could just ignore this error?
return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure)
}
@@ -215,7 +231,14 @@ class BootstrapCrossSigningTask @Inject constructor(
)
)
try {
- if (session.cryptoService().keysBackupService().keysBackupVersion == null) {
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Checking megolm backup")
+
+ // First ensure that in sync
+ val serverVersion = awaitCallback {
+ session.cryptoService().keysBackupService().getCurrentVersion(it)
+ }
+ if (serverVersion == null) {
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
val creationInfo = awaitCallback {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
@@ -223,6 +246,7 @@ class BootstrapCrossSigningTask @Inject constructor(
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
awaitCallback {
@@ -234,11 +258,36 @@ class BootstrapCrossSigningTask @Inject constructor(
)
}
}
+ } else {
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found")
+ // ensure we store existing backup secret if we have it!
+ val knownSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
+ if (knownSecret != null && knownSecret.version == serverVersion.version) {
+ // check it matches
+ val isValid = awaitCallback {
+ session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownSecret.recoveryKey, it)
+ }
+ if (isValid) {
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
+ awaitCallback {
+ extractCurveKeyFromRecoveryKey(knownSecret.recoveryKey)?.toBase64NoPadding()?.let { secret ->
+ ssssService.storeSecret(
+ KEYBACKUP_SECRET_SSSS_NAME,
+ secret,
+ listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it
+ )
+ }
+ }
+ } else {
+ Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key is unknown by this session")
+ }
+ }
}
} catch (failure: Throwable) {
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
}
+ Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished")
return BootstrapResult.Success(keyInfo)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
index 156acf845f..ea558145c0 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
@@ -58,6 +58,13 @@ class BootstrapSetupRecoveryKeyFragment @Inject constructor() : VectorBaseFragme
bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
} else {
+ if (state.step.reset) {
+ bootstrapSetupSecureText.text = getString(R.string.reset_secure_backup_title)
+ bootstrapSetupWarningTextView.isVisible = true
+ } else {
+ bootstrapSetupSecureText.text = getString(R.string.bottom_sheet_setup_secure_backup_subtitle)
+ bootstrapSetupWarningTextView.isVisible = false
+ }
// Choose between create a passphrase or use a recovery key
bootstrapSetupSecureSubmit.isVisible = false
bootstrapSetupSecureUseSecurityKey.isVisible = true
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
index 3a95a575f4..8b247bd975 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
@@ -69,7 +69,11 @@ class BootstrapSharedViewModel @AssistedInject constructor(
init {
- if (args.initCrossSigningOnly) {
+ if (args.forceReset4S) {
+ setState {
+ copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
+ }
+ } else if (args.initCrossSigningOnly) {
// Go straight to account password
setState {
copy(step = BootstrapStep.AccountPassword(false))
@@ -406,7 +410,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
setState {
copy(
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
- step = BootstrapStep.SaveRecoveryKey(false)
+ step = BootstrapStep.SaveRecoveryKey(
+ // If a passphrase was used, saving key is optional
+ state.passphrase != null
+ )
)
}
}
@@ -551,7 +558,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
- ?: BootstrapBottomSheet.Args(initCrossSigningOnly = true)
+ ?: BootstrapBottomSheet.Args(initCrossSigningOnly = true, forceReset4S = false)
return fragment.bootstrapViewModelFactory.create(state, args)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt
index c7639068d1..71b00016ab 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt
@@ -89,7 +89,7 @@ sealed class BootstrapStep {
object CheckingMigration : BootstrapStep()
// Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
- data class FirstForm(val keyBackUpExist: Boolean) : BootstrapStep()
+ data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep()
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
index 7a3d38f649..cd9fed108b 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
@@ -250,7 +250,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
is VerificationTxState.Started,
is VerificationTxState.WaitingOtherReciprocateConfirm -> {
showFragment(VerificationQRWaitingFragment::class, Bundle().apply {
- putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args(state.isMe, state.otherUserMxItem?.getBestName() ?: ""))
+ putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args(
+ isMe = state.isMe,
+ otherUserName = state.otherUserMxItem?.getBestName() ?: ""
+ ))
})
return@withState
}
@@ -353,6 +356,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
}
+ fun forSelfVerification(session: Session, outgoingRequest: String): VerificationBottomSheet {
+ return VerificationBottomSheet().apply {
+ arguments = Bundle().apply {
+ putParcelable(MvRx.KEY_ARG, VerificationArgs(
+ otherUserId = session.myUserId,
+ selfVerificationMode = true,
+ verificationId = outgoingRequest
+ ))
+ }
+ }
+ }
const val WAITING_SELF_VERIF_TAG: String = "WAITING_SELF_VERIF_TAG"
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
index 9b454436d9..53c9deb296 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
@@ -235,11 +235,12 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
pendingRequest = Loading()
)
}
- val roomParams = CreateRoomParams(
- invitedUserIds = listOf(otherUserId)
- )
- .setDirectMessage()
- .enableEncryptionIfInvitedUsersSupportIt()
+ val roomParams = CreateRoomParams()
+ .apply {
+ invitedUserIds.add(otherUserId)
+ setDirectMessage()
+ enableEncryptionIfInvitedUsersSupportIt = true
+ }
session.createRoom(roomParams, object : MatrixCallback {
override fun onSuccess(data: String) {
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
index 88f6607a41..8ac2ce72cb 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
@@ -146,7 +146,7 @@ class VerificationRequestController @Inject constructor(
}
}
- if (state.isMe && state.currentDeviceCanCrossSign) {
+ if (state.isMe && state.currentDeviceCanCrossSign && !state.selfVerificationMode) {
dividerItem {
id("sep_notMe")
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
index 687c280910..3bf2f13d48 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
@@ -30,6 +30,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.util.MatrixItem
+import im.vector.riotx.core.contacts.MappedContact
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest
@@ -63,21 +64,38 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
DrawableImageViewTarget(imageView))
}
+ @UiThread
+ fun render(mappedContact: MappedContact, imageView: ImageView) {
+ // Create a Fake MatrixItem, for the placeholder
+ val matrixItem = MatrixItem.UserItem(
+ // Need an id starting with @
+ id = "@${mappedContact.displayName}",
+ displayName = mappedContact.displayName
+ )
+
+ val placeholder = getPlaceholderDrawable(imageView.context, matrixItem)
+ GlideApp.with(imageView)
+ .load(mappedContact.photoURI)
+ .apply(RequestOptions.circleCropTransform())
+ .placeholder(placeholder)
+ .into(imageView)
+ }
+
@UiThread
fun render(context: Context,
- glideRequest: GlideRequests,
+ glideRequests: GlideRequests,
matrixItem: MatrixItem,
target: Target) {
val placeholder = getPlaceholderDrawable(context, matrixItem)
- buildGlideRequest(glideRequest, matrixItem.avatarUrl)
+ buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.placeholder(placeholder)
.into(target)
}
@AnyThread
@Throws
- fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
- return glideRequest
+ fun shortcutDrawable(context: Context, glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
+ return glideRequests
.asBitmap()
.apply {
val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
@@ -98,8 +116,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
}
@AnyThread
- fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
- return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
+ fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable {
+ return buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.onlyRetrieveFromCache(true)
.submit()
.get()
@@ -117,9 +135,9 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
// PRIVATE API *********************************************************************************
- private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest {
+ private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest {
val resolvedUrl = resolvedUrl(avatarUrl)
- return glideRequest
+ return glideRequests
.load(resolvedUrl)
.apply(RequestOptions.circleCropTransform())
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
index 8d5fc5f564..5bed5b1f78 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
@@ -46,7 +46,8 @@ import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
-import im.vector.riotx.features.workers.signout.SignOutViewModel
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState
import im.vector.riotx.push.fcm.FcmHelper
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.*
@@ -60,13 +61,16 @@ data class HomeActivityArgs(
val accountCreation: Boolean
) : Parcelable
-class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory {
+class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
+ private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
+ @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory
+
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager
@@ -92,6 +96,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
return unknownDeviceViewModelFactory.create(initialState)
}
+ override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
+ return serverBackupviewModelFactory.create(initialState)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
@@ -177,7 +185,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
R.string.crosssigning_verify_this_session,
R.string.confirm_your_identity
) {
- it.navigator.waitSessionVerification(it)
+ if (event.waitForIncomingRequest) {
+ it.navigator.waitSessionVerification(it)
+ } else {
+ it.navigator.requestSelfSessionVerification(it)
+ }
}
}
@@ -230,7 +242,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
}
// Force remote backup state update to update the banner if needed
- viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded()
+ serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
}
override fun configure(toolbar: Toolbar) {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt
index 2f1d8b2705..1cdabe824c 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewEvents.kt
@@ -21,5 +21,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
- data class OnNewSession(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
+ data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
index fdf0936d58..f89bb5a547 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivityViewModel.kt
@@ -130,7 +130,14 @@ class HomeActivityViewModel @AssistedInject constructor(
// Cross-signing is already set up for this user, is it trusted?
if (!mxCrossSigningInfo.isTrusted()) {
// New session
- _viewEvents.post(HomeActivityViewEvents.OnNewSession(session.getUser(session.myUserId)?.toMatrixItem()))
+ _viewEvents.post(
+ HomeActivityViewEvents.OnNewSession(
+ session.getUser(session.myUserId)?.toMatrixItem(),
+ // If it's an old unverified, we should send requests
+ // instead of waiting for an incoming one
+ reAuthHelper.data != null
+ )
+ )
}
} else {
// Initialize cross-signing
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
index c92c28079f..65a599665e 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
@@ -17,22 +17,18 @@
package im.vector.riotx.features.home
import android.os.Bundle
-import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
-import androidx.core.view.forEachIndexed
import androidx.lifecycle.Observer
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
-import com.google.android.material.bottomnavigation.BottomNavigationItemView
-import com.google.android.material.bottomnavigation.BottomNavigationMenuView
-import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
+import com.google.android.material.badge.BadgeDrawable
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
-import im.vector.riotx.core.extensions.commitTransactionNow
+import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
@@ -45,35 +41,34 @@ import im.vector.riotx.features.call.VectorCallActivity
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListParams
-import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert
+import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
-import im.vector.riotx.features.workers.signout.SignOutViewModel
+import im.vector.riotx.features.themes.ThemeUtils
+import im.vector.riotx.features.workers.signout.BannerState
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel
+import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState
import kotlinx.android.synthetic.main.fragment_home_detail.*
-import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiP
-import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiPWrap
-import kotlinx.android.synthetic.main.fragment_home_detail.activeCallView
-import kotlinx.android.synthetic.main.fragment_home_detail.syncStateView
-import kotlinx.android.synthetic.main.fragment_room_detail.*
import timber.log.Timber
import javax.inject.Inject
-private const val INDEX_CATCHUP = 0
-private const val INDEX_PEOPLE = 1
-private const val INDEX_ROOMS = 2
+private const val INDEX_PEOPLE = 0
+private const val INDEX_ROOMS = 1
+private const val INDEX_CATCHUP = 2
class HomeDetailFragment @Inject constructor(
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
+ private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory,
private val avatarRenderer: AvatarRenderer,
private val alertManager: PopupAlertManager,
- private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
-) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback {
-
- private val unreadCounterBadgeViews = arrayListOf()
+ private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
+ private val vectorPreferences: VectorPreferences
+) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory {
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
+ private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
@@ -130,6 +125,25 @@ class HomeDetailFragment @Inject constructor(
})
}
+ override fun onResume() {
+ super.onResume()
+ // update notification tab if needed
+ checkNotificationTabStatus()
+ }
+
+ private fun checkNotificationTabStatus() {
+ val wasVisible = bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible
+ bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
+ if (wasVisible && !vectorPreferences.labAddNotificationTab()) {
+ // As we hide it check if it's not the current item!
+ withState(viewModel) {
+ if (it.displayMode.toMenuId() == R.id.bottom_action_notification) {
+ viewModel.handle(HomeDetailAction.SwitchDisplayMode(RoomListDisplayMode.PEOPLE))
+ }
+ }
+ }
+ }
+
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
@@ -195,34 +209,15 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupKeysBackupBanner() {
- // Keys backup banner
- // Use the SignOutViewModel, it observe the keys backup state and this is what we need here
- val model = fragmentViewModelProvider.get(SignOutViewModel::class.java)
-
- model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState ->
- when (keysBackupState) {
- null ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
- KeysBackupState.Disabled ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(model.getNumberOfKeysToBackup()), false)
- KeysBackupState.NotTrusted,
- KeysBackupState.WrongBackUpVersion ->
- // In this case, getCurrentBackupVersion() should not return ""
- homeKeysBackupBanner.render(KeysBackupBanner.State.Recover(model.getCurrentBackupVersion()), false)
- KeysBackupState.WillBackUp,
- KeysBackupState.BackingUp ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
- KeysBackupState.ReadyToBackUp ->
- if (model.canRestoreKeys()) {
- homeKeysBackupBanner.render(KeysBackupBanner.State.Update(model.getCurrentBackupVersion()), false)
- } else {
- homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
+ serverBackupStatusViewModel
+ .subscribe(this) {
+ when (val banState = it.bannerState.invoke()) {
+ is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
+ BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
+ null,
+ BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
}
- else ->
- homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
- }
- })
-
+ }
homeKeysBackupBanner.delegate = this
}
@@ -247,24 +242,27 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupBottomNavigationView() {
+ bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
bottomNavigationView.setOnNavigationItemSelectedListener {
val displayMode = when (it.itemId) {
R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE
R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS
- else -> RoomListDisplayMode.HOME
+ else -> RoomListDisplayMode.NOTIFICATIONS
}
viewModel.handle(HomeDetailAction.SwitchDisplayMode(displayMode))
true
}
- val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
- menuView.forEachIndexed { index, view ->
- val itemView = view as BottomNavigationItemView
- val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
- val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
- itemView.addView(badgeLayout)
- unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
- }
+// val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
+
+// bottomNavigationView.getOrCreateBadge()
+// menuView.forEachIndexed { index, view ->
+// val itemView = view as BottomNavigationItemView
+// val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
+// val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
+// itemView.addView(badgeLayout)
+// unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
+// }
}
private fun switchDisplayMode(displayMode: RoomListDisplayMode) {
@@ -275,7 +273,7 @@ class HomeDetailFragment @Inject constructor(
private fun updateSelectedFragment(displayMode: RoomListDisplayMode) {
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}"
val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag)
- childFragmentManager.commitTransactionNow {
+ childFragmentManager.commitTransaction {
childFragmentManager.fragments
.filter { it != fragmentToShow }
.forEach {
@@ -304,16 +302,28 @@ class HomeDetailFragment @Inject constructor(
override fun invalidate() = withState(viewModel) {
Timber.v(it.toString())
- unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
- unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
- unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
+ bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople)
+ bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
+ bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
syncStateView.render(it.syncState)
}
+ private fun BadgeDrawable.render(count: Int, highlight: Boolean) {
+ isVisible = count > 0
+ number = count
+ maxCharacterCount = 3
+ badgeTextColor = ContextCompat.getColor(requireContext(), R.color.white)
+ backgroundColor = if (highlight) {
+ ContextCompat.getColor(requireContext(), R.color.riotx_notice)
+ } else {
+ ThemeUtils.getColor(requireContext(), R.attr.riotx_unread_room_badge)
+ }
+ }
+
private fun RoomListDisplayMode.toMenuId() = when (this) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
- else -> R.id.bottom_action_home
+ else -> R.id.bottom_action_notification
}
override fun onTapToReturnToCall() {
@@ -331,4 +341,8 @@ class HomeDetailFragment @Inject constructor(
}
}
}
+
+ override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
+ return serverBackupStatusViewModelFactory.create(initialState)
+ }
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
index 6d7f49750d..365eda74a8 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
@@ -20,7 +20,7 @@ import androidx.annotation.StringRes
import im.vector.riotx.R
enum class RoomListDisplayMode(@StringRes val titleRes: Int) {
- HOME(R.string.bottom_action_home),
+ NOTIFICATIONS(R.string.bottom_action_notification),
PEOPLE(R.string.bottom_action_people_x),
ROOMS(R.string.bottom_action_rooms),
FILTERED(/* Not used */ 0)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
index 4be5502678..50a28b8a8b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
@@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.riotx.core.utils.Debouncer
-import timber.log.Timber
/**
* Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event
@@ -67,7 +66,6 @@ class JumpToBottomViewVisibilityManager(
}
private fun maybeShowJumpToBottomViewVisibility() {
- Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
if (layoutManager.findFirstVisibleItemPosition() != 0) {
jumpToBottomView.show()
} else {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index d38a26c099..3c65b6281f 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -69,6 +69,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.room.model.Membership
+import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
@@ -636,7 +637,7 @@ class RoomDetailFragment @Inject constructor(
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document)
}
- composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody
+ composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
updateComposerText(defaultContent)
@@ -853,12 +854,14 @@ class RoomDetailFragment @Inject constructor(
}
override fun invalidate() = withState(roomDetailViewModel) { state ->
- renderRoomSummary(state)
invalidateOptionsMenu()
val summary = state.asyncRoomSummary()
+ renderToolbar(summary, state.typingMessage)
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
roomWidgetsBannerView.render(state.activeRoomWidgets())
+ jumpToBottomView.count = summary.notificationCount
+ jumpToBottomView.drawBadge = summary.hasUnreadMessages
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state)
inviteView.visibility = View.GONE
@@ -880,7 +883,7 @@ class RoomDetailFragment @Inject constructor(
}
} else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE
- inviteView.render(inviter, VectorInviteView.Mode.LARGE)
+ inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
// Intercept click event
inviteView.setOnClickListener { }
} else if (state.asyncInviter.complete) {
@@ -888,15 +891,15 @@ class RoomDetailFragment @Inject constructor(
}
}
- private fun renderRoomSummary(state: RoomDetailViewState) {
- state.asyncRoomSummary()?.let { roomSummary ->
+ private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) {
+ if (roomSummary == null) {
+ roomToolbarContentView.isClickable = false
+ } else {
+ roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
roomToolbarTitleView.text = roomSummary.displayName
avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView)
- renderSubTitle(state.typingMessage, roomSummary.topic)
- jumpToBottomView.count = roomSummary.notificationCount
- jumpToBottomView.drawBadge = roomSummary.hasUnreadMessages
-
+ renderSubTitle(typingMessage, roomSummary.topic)
roomToolbarDecorationImageView.let {
it.setImageResource(roomSummary.roomEncryptionTrustLevel.toImageRes())
it.isVisible = roomSummary.roomEncryptionTrustLevel != null
@@ -957,7 +960,7 @@ class RoomDetailFragment @Inject constructor(
updateComposerText("")
}
is RoomDetailViewEvents.SlashCommandResultError -> {
- displayCommandError(sendMessageResult.throwable.localizedMessage ?: getString(R.string.unexpected_error))
+ displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
}
is RoomDetailViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented))
@@ -1171,14 +1174,27 @@ class RoomDetailFragment @Inject constructor(
}
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
- navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = roomDetailArgs.roomId,
+ mediaData = mediaData,
+ view = view
+ ) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
}
}
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
- navigator.openVideoViewer(requireActivity(), mediaData)
+ navigator.openMediaViewer(
+ activity = requireActivity(),
+ roomId = roomDetailArgs.roomId,
+ mediaData = mediaData,
+ view = view
+ ) { pairs ->
+ pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
+ pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
+ }
}
// override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
@@ -1196,7 +1212,7 @@ class RoomDetailFragment @Inject constructor(
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
if (allGranted(grantResults)) {
when (requestCode) {
- SAVE_ATTACHEMENT_REQUEST_CODE -> {
+ SAVE_ATTACHEMENT_REQUEST_CODE -> {
sharedActionViewModel.pendingAction?.let {
handleActions(it)
sharedActionViewModel.pendingAction = null
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index e2e7700d1f..a396152f6b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
@@ -166,6 +167,7 @@ class RoomDetailViewModel @AssistedInject constructor(
timeline.start()
timeline.addListener(this)
observeRoomSummary()
+ observeMembershipChanges()
observeSummaryState()
getUnreadState()
observeSyncState()
@@ -405,17 +407,22 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
- fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
- R.id.clear_message_queue ->
- // For now always disable when not in developer mode, worker cancellation is not working properly
- timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
- R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
- R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
- R.id.open_matrix_apps -> true
- R.id.voice_call,
- R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null
- R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
- else -> false
+ fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->
+ if (state.asyncRoomSummary()?.membership != Membership.JOIN) {
+ return@withState false
+ }
+ when (itemId) {
+ R.id.clear_message_queue ->
+ // For now always disable when not in developer mode, worker cancellation is not working properly
+ timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
+ R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
+ R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
+ R.id.open_matrix_apps -> true
+ R.id.voice_call,
+ R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null
+ R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
+ else -> false
+ }
}
// PRIVATE METHODS *****************************************************************************
@@ -450,6 +457,10 @@ class RoomDetailViewModel @AssistedInject constructor(
handleInviteSlashCommand(slashCommandResult)
popDraft()
}
+ is ParsedCommand.Invite3Pid -> {
+ handleInvite3pidSlashCommand(slashCommandResult)
+ popDraft()
+ }
is ParsedCommand.SetUserPowerLevel -> {
handleSetUserPowerLevel(slashCommandResult)
popDraft()
@@ -624,7 +635,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
- session.joinRoom(command.roomAlias, command.reason, object : MatrixCallback {
+ session.joinRoom(command.roomAlias, command.reason, emptyList(), object : MatrixCallback {
override fun onSuccess(data: Unit) {
session.getRoomSummary(command.roomAlias)
?.roomId
@@ -671,6 +682,12 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
+ private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
+ launchSlashCommandFlow {
+ room.invite3pid(invite.threePid, it)
+ }
+ }
+
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
val currentPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
?.content
@@ -846,17 +863,14 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
- private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) {
- setState { copy(sendMode = SendMode.REGULAR(action.text)) }
- withState { state ->
- // For edit, just delete the current draft
- if (state.sendMode is SendMode.EDIT) {
- room.deleteDraft(NoOpMatrixCallback())
- } else {
- // Save a new draft and keep the previously entered text
- room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback())
- }
+ private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) = withState {
+ if (it.sendMode is SendMode.EDIT) {
+ room.deleteDraft(NoOpMatrixCallback())
+ } else {
+ // Save a new draft and keep the previously entered text
+ room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback())
}
+ setState { copy(sendMode = SendMode.REGULAR(action.text)) }
}
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {
@@ -1145,6 +1159,19 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
+ private fun observeMembershipChanges() {
+ session.rx()
+ .liveRoomChangeMembershipState()
+ .map {
+ it[initialState.roomId] ?: ChangeMembershipState.Unknown
+ }
+ .distinctUntilChanged()
+ .subscribe {
+ setState { copy(changeMembershipState = it) }
+ }
+ .disposeOnClear()
+ }
+
private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
roomSummaryHolder.set(summary)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
index 224dd61b65..6800850c48 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
@@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.events.model.Event
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@@ -64,6 +65,7 @@ data class RoomDetailViewState(
val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true,
+ val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
val canSendMessage: Boolean = true
) : MvRxState {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 2174556098..4f5f34cbf0 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -337,7 +337,7 @@ class MessageItemFactory @Inject constructor(
.playable(true)
.highlighted(highlight)
.mediaData(thumbnailData)
- .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
+ .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
}
private fun buildItemForTextContent(messageContent: MessageTextContent,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 22fd4eb5ec..72da87415c 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -50,6 +50,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_AVATAR,
EventType.STATE_ROOM_MEMBER,
+ EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STATE_ROOM_ALIASES,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_JOIN_RULES,
@@ -96,8 +97,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
verificationConclusionItemFactory.create(event, highlight, callback)
}
- // Unhandled event types (yet)
- EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback)
+ // Unhandled event types
else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("Type ${event.root.getClearType()} not handled")
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index c1f4187e0b..090e2dda3f 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.RoomJoinRules
import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.RoomNameContent
+import im.vector.matrix.android.api.session.room.model.RoomThirdPartyInviteContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
@@ -40,17 +41,20 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
+import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.R
-import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import timber.log.Timber
import javax.inject.Inject
-class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
+class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val sp: StringProvider) {
- private fun Event.isSentByCurrentUser() = senderId != null && senderId == sessionHolder.getSafeActiveSession()?.myUserId
+ private val currentUserId: String?
+ get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
+
+ private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) {
@@ -60,6 +64,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
+ EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
@@ -92,7 +97,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? {
val powerLevelsContent: PowerLevelsContent = event.getClearContent().toModel() ?: return null
- val previousPowerLevelsContent: PowerLevelsContent = event.prevContent.toModel() ?: return null
+ val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null
val userIds = HashSet()
userIds.addAll(powerLevelsContent.users.keys)
userIds.addAll(previousPowerLevelsContent.users.keys)
@@ -120,7 +125,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? {
val widgetContent: WidgetContent = event.getClearContent().toModel() ?: return null
- val previousWidgetContent: WidgetContent? = event.prevContent.toModel()
+ val previousWidgetContent: WidgetContent? = event.resolvedPrevContent().toModel()
return if (widgetContent.isActive()) {
val widgetName = widgetContent.getHumanName()
if (previousWidgetContent?.isActive().orFalse()) {
@@ -153,6 +158,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName)
+ EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
@@ -251,11 +257,36 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
}
+ private fun formatRoomThirdPartyInvite(event: Event, senderName: String?): CharSequence? {
+ val content = event.getClearContent().toModel()
+ val prevContent = event.resolvedPrevContent()?.toModel()
+
+ return when {
+ prevContent != null -> {
+ // Revoke case
+ if (event.isSentByCurrentUser()) {
+ sp.getString(R.string.notice_room_third_party_revoked_invite_by_you, prevContent.displayName)
+ } else {
+ sp.getString(R.string.notice_room_third_party_revoked_invite, senderName, prevContent.displayName)
+ }
+ }
+ content != null -> {
+ // Invitation case
+ if (event.isSentByCurrentUser()) {
+ sp.getString(R.string.notice_room_third_party_invite_by_you, content.displayName)
+ } else {
+ sp.getString(R.string.notice_room_third_party_invite, senderName, content.displayName)
+ }
+ }
+ else -> null
+ }
+ }
+
private fun formatCallEvent(type: String, event: Event, senderName: String?): CharSequence? {
return when (type) {
EventType.CALL_INVITE -> {
val content = event.getClearContent().toModel() ?: return null
- val isVideoCall = content.offer?.sdp == CallInviteContent.Offer.SDP_VIDEO
+ val isVideoCall = content.isVideo()
return if (isVideoCall) {
if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_placed_video_call_by_you)
@@ -294,7 +325,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
val eventContent: RoomMemberContent? = event.getClearContent().toModel()
- val prevEventContent: RoomMemberContent? = event.prevContent.toModel()
+ val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, senderName, eventContent, prevEventContent)
@@ -305,7 +336,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? {
val eventContent: RoomAliasesContent? = event.getClearContent().toModel()
- val prevEventContent: RoomAliasesContent? = event.unsignedData?.prevContent?.toModel()
+ val prevEventContent: RoomAliasesContent? = event.resolvedPrevContent()?.toModel()
val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty()
val removedAliases = prevEventContent?.aliases.orEmpty() - eventContent?.aliases.orEmpty()
@@ -449,7 +480,6 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when (eventContent?.membership) {
Membership.INVITE -> {
- val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
when {
eventContent.thirdPartyInvite != null -> {
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
@@ -466,7 +496,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName)
}
}
- event.stateKey == selfUserId ->
+ event.stateKey == currentUserId ->
eventContent.safeReason?.let { reason ->
sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason)
} ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt
index c52b863658..bee3ca6c5b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt
@@ -22,6 +22,7 @@ import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
+import androidx.core.content.withStyledAttributes
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotx.R
@@ -73,11 +74,11 @@ class PollResultLineView @JvmOverloads constructor(
orientation = HORIZONTAL
ButterKnife.bind(this)
- val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PollResultLineView, 0, 0)
- label = typedArray.getString(R.styleable.PollResultLineView_optionName) ?: ""
- percent = typedArray.getString(R.styleable.PollResultLineView_optionCount) ?: ""
- optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false)
- isWinner = typedArray.getBoolean(R.styleable.PollResultLineView_optionIsWinner, false)
- typedArray.recycle()
+ context.withStyledAttributes(attrs, R.styleable.PollResultLineView) {
+ label = getString(R.styleable.PollResultLineView_optionName) ?: ""
+ percent = getString(R.styleable.PollResultLineView_optionCount) ?: ""
+ optionSelected = getBoolean(R.styleable.PollResultLineView_optionSelected, false)
+ isWinner = getBoolean(R.styleable.PollResultLineView_optionIsWinner, false)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt
index 4e4e758aa2..7338a46d8a 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt
@@ -19,9 +19,9 @@ package im.vector.riotx.features.home.room.list
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
-import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
@@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.ButtonStateView
import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.invite.InviteButtonStateBinder
@EpoxyModelClass(layout = R.layout.item_room_invitation)
abstract class RoomInvitationItem : VectorEpoxyModel() {
@@ -37,53 +38,36 @@ abstract class RoomInvitationItem : VectorEpoxyModel(
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var secondLine: CharSequence? = null
@EpoxyAttribute var listener: (() -> Unit)? = null
- @EpoxyAttribute var invitationAcceptInProgress: Boolean = false
- @EpoxyAttribute var invitationAcceptInError: Boolean = false
- @EpoxyAttribute var invitationRejectInProgress: Boolean = false
- @EpoxyAttribute var invitationRejectInError: Boolean = false
+ @EpoxyAttribute lateinit var changeMembershipState: ChangeMembershipState
@EpoxyAttribute var acceptListener: (() -> Unit)? = null
@EpoxyAttribute var rejectListener: (() -> Unit)? = null
+ private val acceptCallback = object : ButtonStateView.Callback {
+ override fun onButtonClicked() {
+ acceptListener?.invoke()
+ }
+
+ override fun onRetryClicked() {
+ acceptListener?.invoke()
+ }
+ }
+
+ private val rejectCallback = object : ButtonStateView.Callback {
+ override fun onButtonClicked() {
+ rejectListener?.invoke()
+ }
+
+ override fun onRetryClicked() {
+ rejectListener?.invoke()
+ }
+ }
+
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
-
- // When a request is in progress (accept or reject), we only use the accept State button
- val requestInProgress = invitationAcceptInProgress || invitationRejectInProgress
-
- when {
- requestInProgress -> holder.acceptView.render(ButtonStateView.State.Loading)
- invitationAcceptInError -> holder.acceptView.render(ButtonStateView.State.Error)
- else -> holder.acceptView.render(ButtonStateView.State.Button)
- }
- // ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
-
- holder.acceptView.callback = object : ButtonStateView.Callback {
- override fun onButtonClicked() {
- acceptListener?.invoke()
- }
-
- override fun onRetryClicked() {
- acceptListener?.invoke()
- }
- }
-
- holder.rejectView.isVisible = !requestInProgress
-
- when {
- invitationRejectInError -> holder.rejectView.render(ButtonStateView.State.Error)
- else -> holder.rejectView.render(ButtonStateView.State.Button)
- }
-
- holder.rejectView.callback = object : ButtonStateView.Callback {
- override fun onButtonClicked() {
- rejectListener?.invoke()
- }
-
- override fun onRetryClicked() {
- rejectListener?.invoke()
- }
- }
+ holder.acceptView.callback = acceptCallback
+ holder.rejectView.callback = rejectCallback
+ InviteButtonStateBinder.bind(holder.acceptView, holder.rejectView, changeMembershipState)
holder.titleView.text = matrixItem.getBestName()
holder.subtitleView.setTextOrHide(secondLine)
avatarRenderer.render(matrixItem, holder.avatarImageView)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
index 3045987d01..dd75deb8ee 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
@@ -28,9 +28,9 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) :
return false
}
return when (displayMode) {
- RoomListDisplayMode.HOME ->
+ RoomListDisplayMode.NOTIFICATIONS ->
roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE || roomSummary.userDrafts.isNotEmpty()
- RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership.isActive()
+ RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership.isActive()
RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership.isActive()
RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
index b31117f18f..2858097e24 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
@@ -138,8 +138,8 @@ class RoomListFragment @Inject constructor(
private fun setupCreateRoomButton() {
when (roomListParams.displayMode) {
- RoomListDisplayMode.HOME -> createChatFabMenu.isVisible = true
- RoomListDisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
+ RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.isVisible = true
+ RoomListDisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
RoomListDisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
else -> Unit // No button in this mode
}
@@ -164,8 +164,8 @@ class RoomListFragment @Inject constructor(
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) {
- RoomListDisplayMode.HOME -> createChatFabMenu.hide()
- RoomListDisplayMode.PEOPLE -> createChatRoomButton.hide()
+ RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.hide()
+ RoomListDisplayMode.PEOPLE -> createChatRoomButton.hide()
RoomListDisplayMode.ROOMS -> createGroupRoomButton.hide()
else -> Unit
}
@@ -207,8 +207,8 @@ class RoomListFragment @Inject constructor(
private val showFabRunnable = Runnable {
if (isAdded) {
when (roomListParams.displayMode) {
- RoomListDisplayMode.HOME -> createChatFabMenu.show()
- RoomListDisplayMode.PEOPLE -> createChatRoomButton.show()
+ RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.show()
+ RoomListDisplayMode.PEOPLE -> createChatRoomButton.show()
RoomListDisplayMode.ROOMS -> createGroupRoomButton.show()
else -> Unit
}
@@ -258,7 +258,7 @@ class RoomListFragment @Inject constructor(
roomController.update(state)
// Mark all as read menu
when (roomListParams.displayMode) {
- RoomListDisplayMode.HOME,
+ RoomListDisplayMode.NOTIFICATIONS,
RoomListDisplayMode.PEOPLE,
RoomListDisplayMode.ROOMS -> {
val newValue = state.hasUnread
@@ -288,7 +288,7 @@ class RoomListFragment @Inject constructor(
}
.isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) {
- RoomListDisplayMode.HOME -> {
+ RoomListDisplayMode.NOTIFICATIONS -> {
if (hasNoRoom) {
StateView.State.Empty(
getString(R.string.room_list_catchup_welcome_title),
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
index a2de7c79a0..cfc76b61a8 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
@@ -21,10 +21,12 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
+import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
+import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource
@@ -55,6 +57,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
init {
observeRoomSummaries()
+ observeMembershipChanges()
}
override fun handle(action: RoomListAction) {
@@ -102,37 +105,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
.observeOn(Schedulers.computation())
.map { buildRoomSummaries(it) }
.execute { async ->
- val invitedRooms = async()?.get(RoomCategory.INVITE)?.map { it.roomId }.orEmpty()
- val remainingJoining = joiningRoomsIds.intersect(invitedRooms)
- val remainingJoinErrors = joiningErrorRoomsIds.intersect(invitedRooms)
- val remainingRejecting = rejectingRoomsIds.intersect(invitedRooms)
- val remainingRejectErrors = rejectingErrorRoomsIds.intersect(invitedRooms)
- copy(
- asyncFilteredRooms = async,
- joiningRoomsIds = remainingJoining,
- joiningErrorRoomsIds = remainingJoinErrors,
- rejectingRoomsIds = remainingRejecting,
- rejectingErrorRoomsIds = remainingRejectErrors
- )
+ copy(asyncFilteredRooms = async)
}
}
private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
-
- if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
+ val roomMembershipChange = state.roomMembershipChanges[roomId]
+ if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
- setState {
- copy(
- joiningRoomsIds = joiningRoomsIds + roomId,
- rejectingErrorRoomsIds = rejectingErrorRoomsIds - roomId
- )
- }
-
session.getRoom(roomId)?.join(callback = object : MatrixCallback {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
@@ -142,32 +127,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
override fun onFailure(failure: Throwable) {
// Notify the user
_viewEvents.post(RoomListViewEvents.Failure(failure))
- setState {
- copy(
- joiningRoomsIds = joiningRoomsIds - roomId,
- joiningErrorRoomsIds = joiningErrorRoomsIds + roomId
- )
- }
}
})
}
private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
-
- if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
+ val roomMembershipChange = state.roomMembershipChanges[roomId]
+ if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen
- Timber.w("Try to reject an already rejecting room. Should not happen")
+ Timber.w("Try to left an already leaving or joining room. Should not happen")
return@withState
}
- setState {
- copy(
- rejectingRoomsIds = rejectingRoomsIds + roomId,
- joiningErrorRoomsIds = joiningErrorRoomsIds - roomId
- )
- }
-
session.getRoom(roomId)?.leave(null, object : MatrixCallback {
override fun onSuccess(data: Unit) {
// We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
@@ -179,12 +151,6 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
override fun onFailure(failure: Throwable) {
// Notify the user
_viewEvents.post(RoomListViewEvents.Failure(failure))
- setState {
- copy(
- rejectingRoomsIds = rejectingRoomsIds - roomId,
- rejectingErrorRoomsIds = rejectingErrorRoomsIds + roomId
- )
- }
}
})
}
@@ -235,6 +201,16 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
})
}
+ private fun observeMembershipChanges() {
+ session.rx()
+ .liveRoomChangeMembershipState()
+ .subscribe {
+ Timber.v("ChangeMembership states: $it")
+ setState { copy(roomMembershipChanges = it) }
+ }
+ .disposeOnClear()
+ }
+
private fun buildRoomSummaries(rooms: List): RoomSummaries {
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList()
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
index b41b4b9eeb..63f0cf2a1a 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
@@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
@@ -30,14 +31,7 @@ data class RoomListViewState(
val asyncRooms: Async> = Uninitialized,
val roomFilter: String = "",
val asyncFilteredRooms: Async = Uninitialized,
- // List of roomIds that the user wants to join
- val joiningRoomsIds: Set = emptySet(),
- // List of roomIds that the user wants to join, but an error occurred
- val joiningErrorRoomsIds: Set = emptySet(),
- // List of roomIds that the user wants to join
- val rejectingRoomsIds: Set = emptySet(),
- // List of roomIds that the user wants to reject, but an error occurred
- val rejectingErrorRoomsIds: Set = emptySet(),
+ val roomMembershipChanges: Map = emptyMap(),
val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
index b06cb8a4bb..efa19d012b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
@@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyController
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
@@ -72,10 +73,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries,
- viewState.joiningRoomsIds,
- viewState.joiningErrorRoomsIds,
- viewState.rejectingRoomsIds,
- viewState.rejectingErrorRoomsIds,
+ viewState.roomMembershipChanges,
emptySet())
addFilterFooter(viewState)
@@ -94,10 +92,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
if (isExpanded) {
buildRoomModels(summaries,
- viewState.joiningRoomsIds,
- viewState.joiningErrorRoomsIds,
- viewState.rejectingRoomsIds,
- viewState.rejectingErrorRoomsIds,
+ viewState.roomMembershipChanges,
emptySet())
// Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) {
@@ -153,18 +148,12 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
private fun buildRoomModels(summaries: List,
- joiningRoomsIds: Set,
- joiningErrorRoomsIds: Set,
- rejectingRoomsIds: Set,
- rejectingErrorRoomsIds: Set,
+ roomChangedMembershipStates: Map,
selectedRoomIds: Set) {
summaries.forEach { roomSummary ->
roomSummaryItemFactory
.create(roomSummary,
- joiningRoomsIds,
- joiningErrorRoomsIds,
- rejectingRoomsIds,
- rejectingErrorRoomsIds,
+ roomChangedMembershipStates,
selectedRoomIds,
listener)
.addTo(this)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
index 1830899d80..f33166504d 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
@@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.list
import android.view.View
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.toMatrixItem
@@ -39,23 +40,20 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
private val avatarRenderer: AvatarRenderer) {
fun create(roomSummary: RoomSummary,
- joiningRoomsIds: Set,
- joiningErrorRoomsIds: Set,
- rejectingRoomsIds: Set,
- rejectingErrorRoomsIds: Set,
+ roomChangeMembershipStates: Map,
selectedRoomIds: Set,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
- Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
+ Membership.INVITE -> {
+ val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown
+ createInvitationItem(roomSummary, changeMembershipState, listener)
+ }
else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
}
}
- fun createInvitationItem(roomSummary: RoomSummary,
- joiningRoomsIds: Set,
- joiningErrorRoomsIds: Set,
- rejectingRoomsIds: Set,
- rejectingErrorRoomsIds: Set,
+ private fun createInvitationItem(roomSummary: RoomSummary,
+ changeMembershipState: ChangeMembershipState,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.inviterId
@@ -70,10 +68,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.avatarRenderer(avatarRenderer)
.matrixItem(roomSummary.toMatrixItem())
.secondLine(secondLine)
- .invitationAcceptInProgress(joiningRoomsIds.contains(roomSummary.roomId))
- .invitationAcceptInError(joiningErrorRoomsIds.contains(roomSummary.roomId))
- .invitationRejectInProgress(rejectingRoomsIds.contains(roomSummary.roomId))
- .invitationRejectInError(rejectingErrorRoomsIds.contains(roomSummary.roomId))
+ .changeMembershipState(changeMembershipState)
.acceptListener { listener?.onAcceptRoomInvitation(roomSummary) }
.rejectListener { listener?.onRejectRoomInvitation(roomSummary) }
.listener { listener?.onRoomClicked(roomSummary) }
diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteButtonStateBinder.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteButtonStateBinder.kt
new file mode 100644
index 0000000000..88abf28888
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteButtonStateBinder.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.invite
+
+import androidx.core.view.isInvisible
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
+import im.vector.riotx.core.platform.ButtonStateView
+
+object InviteButtonStateBinder {
+
+ fun bind(
+ acceptView: ButtonStateView,
+ rejectView: ButtonStateView,
+ changeMembershipState: ChangeMembershipState
+ ) {
+ // When a request is in progress (accept or reject), we only use the accept State button
+ // We check for isSuccessful, otherwise we get a glitch the time room summaries get rebuilt
+
+ val requestInProgress = changeMembershipState.isInProgress() || changeMembershipState.isSuccessful()
+ when {
+ requestInProgress -> acceptView.render(ButtonStateView.State.Loading)
+ changeMembershipState is ChangeMembershipState.FailedJoining -> acceptView.render(ButtonStateView.State.Error)
+ else -> acceptView.render(ButtonStateView.State.Button)
+ }
+ // ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
+
+ rejectView.isInvisible = requestInProgress
+
+ when {
+ changeMembershipState is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error)
+ else -> rejectView.render(ButtonStateView.State.Button)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
index 8a62935bdd..6c059c917f 100644
--- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
@@ -16,9 +16,9 @@
package im.vector.riotx.features.invite
-import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.core.platform.VectorViewModelAction
+import im.vector.riotx.features.userdirectory.PendingInvitee
sealed class InviteUsersToRoomAction : VectorViewModelAction {
- data class InviteSelectedUsers(val selectedUsers: Set) : InviteUsersToRoomAction()
+ data class InviteSelectedUsers(val invitees: Set) : InviteUsersToRoomAction()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
index 839a0767d8..af78457d96 100644
--- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
@@ -30,9 +30,16 @@ import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack
+import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData
+import im.vector.riotx.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
+import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
+import im.vector.riotx.core.utils.allGranted
+import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.toast
+import im.vector.riotx.features.contactsbook.ContactsBookFragment
+import im.vector.riotx.features.contactsbook.ContactsBookViewModel
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
@@ -53,6 +60,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
+ @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
@@ -74,7 +82,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
UserDirectorySharedAction.Close -> finish()
UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
- }
+ UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
+ }.exhaustive
}
.disposeOnDestroy()
if (isFirstCreation()) {
@@ -92,9 +101,27 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
viewModel.observeViewEvents { renderInviteEvents(it) }
}
+ private fun openPhoneBook() {
+ // Check permission first
+ if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
+ this,
+ PERMISSION_REQUEST_CODE_READ_CONTACTS,
+ 0)) {
+ addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
+ }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ if (allGranted(grantResults)) {
+ if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
+ addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
+ }
+ }
+ }
+
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_invite_users_to_room_invite) {
- viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selectedUsers))
+ viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
index fc2f34b7a0..2769dc56bb 100644
--- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
@@ -22,11 +22,11 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
-import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.rx.rx
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
+import im.vector.riotx.features.userdirectory.PendingInvitee
import io.reactivex.Observable
class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
@@ -53,27 +53,30 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
override fun handle(action: InviteUsersToRoomAction) {
when (action) {
- is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selectedUsers)
+ is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.invitees)
}
}
- private fun inviteUsersToRoom(selectedUsers: Set) {
+ private fun inviteUsersToRoom(invitees: Set) {
_viewEvents.post(InviteUsersToRoomViewEvents.Loading)
- Observable.fromIterable(selectedUsers).flatMapCompletable { user ->
- room.rx().invite(user.userId, null)
+ Observable.fromIterable(invitees).flatMapCompletable { user ->
+ when (user) {
+ is PendingInvitee.UserPendingInvitee -> room.rx().invite(user.user.userId, null)
+ is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid)
+ }
}.subscribe(
{
- val successMessage = when (selectedUsers.size) {
+ val successMessage = when (invitees.size) {
1 -> stringProvider.getString(R.string.invitation_sent_to_one_user,
- selectedUsers.first().getBestName())
+ invitees.first().getBestName())
2 -> stringProvider.getString(R.string.invitations_sent_to_two_users,
- selectedUsers.first().getBestName(),
- selectedUsers.last().getBestName())
+ invitees.first().getBestName(),
+ invitees.last().getBestName())
else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users,
- selectedUsers.size - 1,
- selectedUsers.first().getBestName(),
- selectedUsers.size - 1)
+ invitees.size - 1,
+ invitees.first().getBestName(),
+ invitees.size - 1)
}
_viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage))
},
diff --git a/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt
index b9bd9b0e1e..42f440fc30 100644
--- a/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt
+++ b/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt
@@ -21,10 +21,12 @@ import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updateLayoutParams
+import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.di.HasScreenInjector
+import im.vector.riotx.core.platform.ButtonStateView
import im.vector.riotx.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.vector_invite_view.view.*
import javax.inject.Inject
@@ -50,11 +52,28 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
context.injector().inject(this)
}
View.inflate(context, R.layout.vector_invite_view, this)
- inviteRejectView.setOnClickListener { callback?.onRejectInvite() }
- inviteAcceptView.setOnClickListener { callback?.onAcceptInvite() }
+ inviteAcceptView.callback = object : ButtonStateView.Callback {
+ override fun onButtonClicked() {
+ callback?.onAcceptInvite()
+ }
+
+ override fun onRetryClicked() {
+ callback?.onAcceptInvite()
+ }
+ }
+
+ inviteRejectView.callback = object : ButtonStateView.Callback {
+ override fun onButtonClicked() {
+ callback?.onRejectInvite()
+ }
+
+ override fun onRetryClicked() {
+ callback?.onRejectInvite()
+ }
+ }
}
- fun render(sender: User, mode: Mode = Mode.LARGE) {
+ fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
if (mode == Mode.LARGE) {
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)
@@ -68,5 +87,6 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
inviteNameView.visibility = View.GONE
inviteLabelView.text = context.getString(R.string.invited_by, sender.userId)
}
+ InviteButtonStateBinder.bind(inviteAcceptView, inviteRejectView, changeMembershipState)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
index 7edc674b11..071e23c252 100644
--- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
@@ -49,9 +49,6 @@ import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.ensureTrailingSlash
-import im.vector.riotx.features.call.WebRtcPeerConnectionManager
-import im.vector.riotx.features.notifications.PushRuleTriggerListener
-import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import timber.log.Timber
import java.util.concurrent.CancellationException
@@ -64,13 +61,10 @@ class LoginViewModel @AssistedInject constructor(
private val applicationContext: Context,
private val authenticationService: AuthenticationService,
private val activeSessionHolder: ActiveSessionHolder,
- private val pushRuleTriggerListener: PushRuleTriggerListener,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
- private val sessionListener: SessionListener,
private val reAuthHelper: ReAuthHelper,
- private val stringProvider: StringProvider,
- private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager)
- : VectorViewModel(initialState) {
+ private val stringProvider: StringProvider
+) : VectorViewModel(initialState) {
@AssistedInject.Factory
interface Factory {
@@ -667,8 +661,7 @@ class LoginViewModel @AssistedInject constructor(
private fun onSessionCreated(session: Session) {
activeSessionHolder.setActiveSession(session)
- session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
- session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
+ session.configureAndStart(applicationContext)
setState {
copy(
asyncLoginAction = Success(Unit)
diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt
new file mode 100644
index 0000000000..2812b011f9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.Group
+import im.vector.riotx.R
+import im.vector.riotx.attachmentviewer.AttachmentEventListener
+import im.vector.riotx.attachmentviewer.AttachmentEvents
+
+class AttachmentOverlayView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
+
+ var onShareCallback: (() -> Unit)? = null
+ var onBack: (() -> Unit)? = null
+ var onPlayPause: ((play: Boolean) -> Unit)? = null
+ var videoSeekTo: ((progress: Int) -> Unit)? = null
+
+ private val counterTextView: TextView
+ private val infoTextView: TextView
+ private val shareImage: ImageView
+ private val overlayPlayPauseButton: ImageView
+ private val overlaySeekBar: SeekBar
+
+ var isPlaying = false
+
+ val videoControlsGroup: Group
+
+ var suspendSeekBarUpdate = false
+
+ init {
+ View.inflate(context, R.layout.merge_image_attachment_overlay, this)
+ setBackgroundColor(Color.TRANSPARENT)
+ counterTextView = findViewById(R.id.overlayCounterText)
+ infoTextView = findViewById(R.id.overlayInfoText)
+ shareImage = findViewById(R.id.overlayShareButton)
+ videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup)
+ overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton)
+ overlaySeekBar = findViewById(R.id.overlaySeekBar)
+ findViewById(R.id.overlayBackButton).setOnClickListener {
+ onBack?.invoke()
+ }
+ findViewById(R.id.overlayShareButton).setOnClickListener {
+ onShareCallback?.invoke()
+ }
+ findViewById(R.id.overlayPlayPauseButton).setOnClickListener {
+ onPlayPause?.invoke(!isPlaying)
+ }
+
+ overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ videoSeekTo?.invoke(progress)
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) {
+ suspendSeekBarUpdate = true
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {
+ suspendSeekBarUpdate = false
+ }
+ })
+ }
+
+ fun updateWith(counter: String, senderInfo: String) {
+ counterTextView.text = counter
+ infoTextView.text = senderInfo
+ }
+
+ override fun onEvent(event: AttachmentEvents) {
+ when (event) {
+ is AttachmentEvents.VideoEvent -> {
+ overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause)
+ if (!suspendSeekBarUpdate) {
+ val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat()
+ val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100)
+ isPlaying = event.isPlaying
+ overlaySeekBar.progress = percent
+ }
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt
new file mode 100644
index 0000000000..d4c41c7cb3
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.ImageView
+import com.bumptech.glide.request.target.CustomViewTarget
+import com.bumptech.glide.request.transition.Transition
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.attachmentviewer.AttachmentSourceProvider
+import im.vector.riotx.attachmentviewer.ImageLoaderTarget
+import im.vector.riotx.attachmentviewer.VideoLoaderTarget
+import java.io.File
+
+abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider {
+
+ interface InteractionListener {
+ fun onDismissTapped()
+ fun onShareTapped()
+ fun onPlayPause(play: Boolean)
+ fun videoSeekTo(percent: Int)
+ }
+
+ var interactionListener: InteractionListener? = null
+
+ protected var overlayView: AttachmentOverlayView? = null
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ if (position == -1) return null
+ if (overlayView == null) {
+ overlayView = AttachmentOverlayView(context)
+ overlayView?.onBack = {
+ interactionListener?.onDismissTapped()
+ }
+ overlayView?.onShareCallback = {
+ interactionListener?.onShareTapped()
+ }
+ overlayView?.onPlayPause = { play ->
+ interactionListener?.onPlayPause(play)
+ }
+ overlayView?.videoSeekTo = { percent ->
+ interactionListener?.videoSeekTo(percent)
+ }
+ }
+ return overlayView
+ }
+
+ override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) {
+ (info.data as? ImageContentRenderer.Data)?.let {
+ imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onResourceReady(info.uid, resource)
+ }
+ })
+ }
+ }
+
+ override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) {
+ (info.data as? ImageContentRenderer.Data)?.let {
+ imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onResourceReady(info.uid, resource)
+ }
+ })
+ }
+ }
+
+ override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) {
+ val data = info.data as? VideoContentRenderer.Data ?: return
+// videoContentRenderer.render(data,
+// holder.thumbnailImage,
+// holder.loaderProgressBar,
+// holder.videoView,
+// holder.errorTextView)
+ imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) {
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ target.onThumbnailLoadFailed(info.uid, errorDrawable)
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ target.onThumbnailResourceCleared(info.uid, placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ target.onThumbnailResourceReady(info.uid, resource)
+ }
+ })
+
+ target.onVideoFileLoading(info.uid)
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
+ id = data.eventId,
+ mimeType = data.mimeType,
+ elementToDecrypt = data.elementToDecrypt,
+ fileName = data.filename,
+ url = data.url,
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ target.onVideoFileReady(info.uid, data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ target.onVideoFileLoadFailed(info.uid)
+ }
+ }
+ )
+ }
+
+ override fun clear(id: String) {
+ // TODO("Not yet implemented")
+ }
+
+ abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit))
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt
new file mode 100644
index 0000000000..cb0039fc7e
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.view.View
+import androidx.core.view.isVisible
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.events.model.isVideoMessage
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.matrix.android.api.session.room.Room
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.core.date.VectorDateFormatter
+import im.vector.riotx.core.extensions.localDateTime
+import java.io.File
+
+class DataAttachmentRoomProvider(
+ private val attachments: List,
+ private val room: Room?,
+ private val initialIndex: Int,
+ imageContentRenderer: ImageContentRenderer,
+ private val dateFormatter: VectorDateFormatter,
+ fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) {
+
+ override fun getItemCount(): Int = attachments.size
+
+ override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
+ return attachments[position].let {
+ when (it) {
+ is ImageContentRenderer.Data -> {
+ if (it.mimeType == "image/gif") {
+ AttachmentInfo.AnimatedImage(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it
+ )
+ }
+ }
+ is VideoContentRenderer.Data -> {
+ AttachmentInfo.Video(
+ uid = it.eventId,
+ url = it.url ?: "",
+ data = it,
+ thumbnail = AttachmentInfo.Image(
+ uid = it.eventId,
+ url = it.thumbnailMediaData.url ?: "",
+ data = it.thumbnailMediaData
+ )
+ )
+ }
+ else -> throw IllegalArgumentException()
+ }
+ }
+ }
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ super.overlayViewAtPosition(context, position)
+ val item = attachments[position]
+ val timeLineEvent = room?.getTimeLineEvent(item.eventId)
+ if (timeLineEvent != null) {
+ val dateString = timeLineEvent.root.localDateTime().let {
+ "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
+ }
+ overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString")
+ overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage()
+ } else {
+ overlayView?.updateWith("", "")
+ }
+ return overlayView
+ }
+
+ override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
+ val item = attachments[position]
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = item.eventId,
+ fileName = item.filename,
+ mimeType = item.mimeType,
+ url = item.url ?: "",
+ elementToDecrypt = item.elementToDecrypt,
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ callback(data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ }
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
index eeeb55ed15..f9cb6ec3dc 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
@@ -19,11 +19,13 @@ package im.vector.riotx.features.media
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Parcelable
+import android.view.View
import android.widget.ImageView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
import com.github.piasy.biv.view.BigImageView
@@ -42,21 +44,29 @@ import java.io.File
import javax.inject.Inject
import kotlin.math.min
+interface AttachmentData : Parcelable {
+ val eventId: String
+ val filename: String
+ val mimeType: String?
+ val url: String?
+ val elementToDecrypt: ElementToDecrypt?
+}
+
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter) {
@Parcelize
data class Data(
- val eventId: String,
- val filename: String,
- val mimeType: String?,
- val url: String?,
- val elementToDecrypt: ElementToDecrypt?,
+ override val eventId: String,
+ override val filename: String,
+ override val mimeType: String?,
+ override val url: String?,
+ override val elementToDecrypt: ElementToDecrypt?,
val height: Int?,
val maxHeight: Int,
val width: Int?,
val maxWidth: Int
- ) : Parcelable {
+ ) : AttachmentData {
fun isLocalFile() = url.isLocalFile()
}
@@ -93,6 +103,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) {
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(contextView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(contextView)
+ .load(resolvedUrl)
+ }
+
+ req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+ .fitCenter()
+ .into(target)
+ }
+
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
val size = processSize(data, mode)
@@ -122,6 +151,49 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ /**
+ * onlyRetrieveFromCache is true!
+ */
+ fun renderForSharedElementTransition(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
+ // a11y
+ imageView.contentDescription = data.filename
+
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(imageView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(imageView)
+ .load(resolvedUrl)
+ }
+
+ req.listener(object : RequestListener {
+ override fun onLoadFailed(e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(false)
+ return false
+ }
+
+ override fun onResourceReady(resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(true)
+ return false
+ }
+ })
+ .onlyRetrieveFromCache(true)
+ .fitCenter()
+ .into(imageView)
+ }
+
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest {
return if (data.elementToDecrypt != null) {
// Encrypted image
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
index 092199759f..8a6c2f7545 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
@@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
encryptedImageView.isVisible = false
// Postpone transaction a bit until thumbnail is loaded
supportPostponeEnterTransition()
+
+ // We are not passing the exact same image that in the
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt
new file mode 100644
index 0000000000..7a7fea6dc4
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.view.View
+import androidx.core.view.isVisible
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.events.model.isVideoMessage
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.file.FileService
+import im.vector.matrix.android.api.session.room.Room
+import im.vector.matrix.android.api.session.room.model.message.MessageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
+import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
+import im.vector.matrix.android.api.session.room.model.message.getFileUrl
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
+import im.vector.riotx.attachmentviewer.AttachmentInfo
+import im.vector.riotx.core.date.VectorDateFormatter
+import im.vector.riotx.core.extensions.localDateTime
+import java.io.File
+import javax.inject.Inject
+
+class RoomEventsAttachmentProvider(
+ private val attachments: List,
+ private val initialIndex: Int,
+ imageContentRenderer: ImageContentRenderer,
+ private val dateFormatter: VectorDateFormatter,
+ fileService: FileService
+) : BaseAttachmentProvider(imageContentRenderer, fileService) {
+
+ override fun getItemCount(): Int {
+ return attachments.size
+ }
+
+ override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
+ return attachments[position].let {
+ val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent
+ if (content is MessageImageContent) {
+ val data = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ maxHeight = -1,
+ maxWidth = -1,
+ width = null,
+ height = null
+ )
+ if (content.mimeType == "image/gif") {
+ AttachmentInfo.AnimatedImage(
+ uid = it.eventId,
+ url = content.url ?: "",
+ data = data
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = content.url ?: "",
+ data = data
+ )
+ }
+ } else if (content is MessageVideoContent) {
+ val thumbnailData = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl,
+ elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
+ height = content.videoInfo?.height,
+ maxHeight = -1,
+ width = content.videoInfo?.width,
+ maxWidth = -1
+ )
+ val data = VideoContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content.body,
+ mimeType = content.mimeType,
+ url = content.getFileUrl(),
+ elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
+ thumbnailMediaData = thumbnailData
+ )
+ AttachmentInfo.Video(
+ uid = it.eventId,
+ url = content.getFileUrl() ?: "",
+ data = data,
+ thumbnail = AttachmentInfo.Image(
+ uid = it.eventId,
+ url = content.videoInfo?.thumbnailFile?.url
+ ?: content.videoInfo?.thumbnailUrl ?: "",
+ data = thumbnailData
+
+ )
+ )
+ } else {
+ AttachmentInfo.Image(
+ uid = it.eventId,
+ url = "",
+ data = null
+ )
+ }
+ }
+ }
+
+ override fun overlayViewAtPosition(context: Context, position: Int): View? {
+ super.overlayViewAtPosition(context, position)
+ val item = attachments[position]
+ val dateString = item.root.localDateTime().let {
+ "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
+ }
+ overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
+ overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
+ return overlayView
+ }
+
+ override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
+ attachments[position].let { timelineEvent ->
+
+ val messageContent = timelineEvent.root.getClearContent().toModel()
+ as? MessageWithAttachmentContent
+ ?: return@let
+ fileService.downloadFile(
+ downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
+ id = timelineEvent.eventId,
+ fileName = messageContent.body,
+ mimeType = messageContent.mimeType,
+ url = messageContent.getFileUrl(),
+ elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: File) {
+ callback(data)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ callback(null)
+ }
+ }
+ )
+ }
+ }
+}
+
+class AttachmentProviderFactory @Inject constructor(
+ private val imageContentRenderer: ImageContentRenderer,
+ private val vectorDateFormatter: VectorDateFormatter,
+ private val session: Session
+) {
+
+ fun createProvider(attachments: List, initialIndex: Int): RoomEventsAttachmentProvider {
+ return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
+ }
+
+ fun createProvider(attachments: List, room: Room?, initialIndex: Int): DataAttachmentRoomProvider {
+ return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
new file mode 100644
index 0000000000..9e5facd162
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import androidx.core.transition.addListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.transition.Transition
+import im.vector.riotx.R
+import im.vector.riotx.attachmentviewer.AttachmentCommands
+import im.vector.riotx.attachmentviewer.AttachmentViewerActivity
+import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.core.di.DaggerScreenComponent
+import im.vector.riotx.core.di.HasVectorInjector
+import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.di.VectorComponent
+import im.vector.riotx.core.intent.getMimeTypeFromUri
+import im.vector.riotx.core.utils.shareMedia
+import im.vector.riotx.features.themes.ActivityOtherThemes
+import im.vector.riotx.features.themes.ThemeUtils
+import kotlinx.android.parcel.Parcelize
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.system.measureTimeMillis
+
+class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
+
+ @Parcelize
+ data class Args(
+ val roomId: String?,
+ val eventId: String,
+ val sharedTransitionName: String?
+ ) : Parcelable
+
+ @Inject
+ lateinit var sessionHolder: ActiveSessionHolder
+
+ @Inject
+ lateinit var dataSourceFactory: AttachmentProviderFactory
+
+ @Inject
+ lateinit var imageContentRenderer: ImageContentRenderer
+
+ private lateinit var screenComponent: ScreenComponent
+
+ private var initialIndex = 0
+ private var isAnimatingOut = false
+
+ var currentSourceProvider: BaseAttachmentProvider? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Timber.i("onCreate Activity ${this.javaClass.simpleName}")
+ val vectorComponent = getVectorComponent()
+ screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
+ val timeForInjection = measureTimeMillis {
+ screenComponent.inject(this)
+ }
+ Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms")
+ ThemeUtils.setActivityTheme(this, getOtherThemes())
+
+ val args = args() ?: throw IllegalArgumentException("Missing arguments")
+
+ if (savedInstanceState == null && addTransitionListener()) {
+ args.sharedTransitionName?.let {
+ ViewCompat.setTransitionName(imageTransitionView, it)
+ transitionImageContainer.isVisible = true
+
+ // Postpone transaction a bit until thumbnail is loaded
+ val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
+ if (mediaData is ImageContentRenderer.Data) {
+ // will be shown at end of transition
+ pager2.isInvisible = true
+ supportPostponeEnterTransition()
+ imageContentRenderer.renderForSharedElementTransition(mediaData, imageTransitionView) {
+ // Proceed with transaction
+ scheduleStartPostponedTransition(imageTransitionView)
+ }
+ } else if (mediaData is VideoContentRenderer.Data) {
+ // will be shown at end of transition
+ pager2.isInvisible = true
+ supportPostponeEnterTransition()
+ imageContentRenderer.renderForSharedElementTransition(mediaData.thumbnailMediaData, imageTransitionView) {
+ // Proceed with transaction
+ scheduleStartPostponedTransition(imageTransitionView)
+ }
+ }
+ }
+ }
+
+ val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
+
+ val room = args.roomId?.let { session.getRoom(it) }
+
+ val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA)
+ if (inMemoryData != null) {
+ val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex)
+ val index = inMemoryData.indexOfFirst { it.eventId == args.eventId }
+ initialIndex = index
+ sourceProvider.interactionListener = this
+ setSourceProvider(sourceProvider)
+ this.currentSourceProvider = sourceProvider
+ if (savedInstanceState == null) {
+ pager2.setCurrentItem(index, false)
+ // The page change listener is not notified of the change...
+ pager2.post {
+ onSelectedPositionChanged(index)
+ }
+ }
+ } else {
+ val events = room?.getAttachmentMessages()
+ ?: emptyList()
+ val index = events.indexOfFirst { it.eventId == args.eventId }
+ initialIndex = index
+
+ val sourceProvider = dataSourceFactory.createProvider(events, index)
+ sourceProvider.interactionListener = this
+ setSourceProvider(sourceProvider)
+ this.currentSourceProvider = sourceProvider
+ if (savedInstanceState == null) {
+ pager2.setCurrentItem(index, false)
+ // The page change listener is not notified of the change...
+ pager2.post {
+ onSelectedPositionChanged(index)
+ }
+ }
+ }
+
+ window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+ window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
+ }
+
+ private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
+
+ override fun shouldAnimateDismiss(): Boolean {
+ return currentPosition != initialIndex
+ }
+
+ override fun onBackPressed() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ super.onBackPressed()
+ }
+
+ override fun animateClose() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ ActivityCompat.finishAfterTransition(this)
+ }
+
+ // ==========================================================================================
+ // PRIVATE METHODS
+ // ==========================================================================================
+
+ /**
+ * Try and add a [Transition.TransitionListener] to the entering shared element
+ * [Transition]. We do this so that we can load the full-size image after the transition
+ * has completed.
+ *
+ * @return true if we were successful in adding a listener to the enter transition
+ */
+ private fun addTransitionListener(): Boolean {
+ val transition = window.sharedElementEnterTransition
+
+ if (transition != null) {
+ // There is an entering shared element transition so add a listener to it
+ transition.addListener(
+ onEnd = {
+ // The listener is also called when we are exiting
+ // so we use a boolean to avoid reshowing pager at end of dismiss transition
+ if (!isAnimatingOut) {
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ },
+ onCancel = {
+ if (!isAnimatingOut) {
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ }
+ )
+ return true
+ }
+
+ // If we reach here then we have not added a listener
+ return false
+ }
+
+ private fun args() = intent.getParcelableExtra(EXTRA_ARGS)
+
+ private fun getVectorComponent(): VectorComponent {
+ return (application as HasVectorInjector).injector()
+ }
+
+ private fun scheduleStartPostponedTransition(sharedElement: View) {
+ sharedElement.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ sharedElement.viewTreeObserver.removeOnPreDrawListener(this)
+ supportStartPostponedEnterTransition()
+ return true
+ }
+ })
+ }
+
+ companion object {
+ const val EXTRA_ARGS = "EXTRA_ARGS"
+ const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
+ const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
+
+ fun newIntent(context: Context,
+ mediaData: AttachmentData,
+ roomId: String?,
+ eventId: String,
+ inMemoryData: List,
+ sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {
+ it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName))
+ it.putExtra(EXTRA_IMAGE_DATA, mediaData)
+ if (inMemoryData.isNotEmpty()) {
+ it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData))
+ }
+ }
+ }
+
+ override fun onDismissTapped() {
+ animateClose()
+ }
+
+ override fun onPlayPause(play: Boolean) {
+ handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
+ }
+
+ override fun videoSeekTo(percent: Int) {
+ handle(AttachmentCommands.SeekTo(percent))
+ }
+
+ override fun onShareTapped() {
+ this.currentSourceProvider?.getFileForSharing(currentPosition) { data ->
+ if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+ shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri()))
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
index 760d3b12a0..e6dec88349 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt
@@ -16,7 +16,6 @@
package im.vector.riotx.features.media
-import android.os.Parcelable
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
@@ -38,13 +37,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
@Parcelize
data class Data(
- val eventId: String,
- val filename: String,
- val mimeType: String?,
- val url: String?,
- val elementToDecrypt: ElementToDecrypt?,
+ override val eventId: String,
+ override val filename: String,
+ override val mimeType: String?,
+ override val url: String?,
+ override val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data
- ) : Parcelable
+ ) : AttachmentData
fun render(data: Data,
thumbnailView: ImageView,
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
index 0b89ab8ec4..8267ba4c99 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
@@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.content.Intent
-import android.os.Build
import android.view.View
import android.view.Window
import androidx.core.app.ActivityOptionsCompat
@@ -29,6 +28,7 @@ import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
+import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.MatrixItem
@@ -49,11 +49,9 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
+import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.media.BigImageViewerActivity
-import im.vector.riotx.features.media.ImageContentRenderer
-import im.vector.riotx.features.media.ImageMediaViewerActivity
-import im.vector.riotx.features.media.VideoContentRenderer
-import im.vector.riotx.features.media.VideoMediaViewerActivity
+import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@@ -89,7 +87,8 @@ class DefaultNavigator @Inject constructor(
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return
- val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
+ val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId)
+ ?: return
(tx as? IncomingSasVerificationTransaction)?.performAccept()
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
@@ -116,6 +115,27 @@ class DefaultNavigator @Inject constructor(
}
}
+ override fun requestSelfSessionVerification(context: Context) {
+ val session = sessionHolder.getSafeActiveSession() ?: return
+ val otherSessions = session.cryptoService()
+ .getCryptoDeviceInfo(session.myUserId)
+ .filter { it.deviceId != session.sessionParams.deviceId }
+ .map { it.deviceId }
+ if (context is VectorBaseActivity) {
+ if (otherSessions.isNotEmpty()) {
+ val pr = session.cryptoService().verificationService().requestKeyVerification(
+ supportedVerificationMethodsProvider.provide(),
+ session.myUserId,
+ otherSessions)
+ VerificationBottomSheet.forSelfVerification(session, pr.transactionId ?: pr.localId)
+ .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
+ } else {
+ VerificationBottomSheet.forSelfVerification(session)
+ .show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
+ }
+ }
+ }
+
override fun waitSessionVerification(context: Context) {
val session = sessionHolder.getSafeActiveSession() ?: return
if (context is VectorBaseActivity) {
@@ -126,7 +146,7 @@ class DefaultNavigator @Inject constructor(
override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
if (context is VectorBaseActivity) {
- BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly)
+ BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly, false)
}
}
@@ -159,8 +179,8 @@ class DefaultNavigator @Inject constructor(
activity.finish()
}
- override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
- val intent = RoomPreviewActivity.getIntent(context, publicRoom)
+ override fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData) {
+ val intent = RoomPreviewActivity.getIntent(context, publicRoom, roomDirectoryData)
context.startActivity(intent)
}
@@ -199,7 +219,14 @@ class DefaultNavigator @Inject constructor(
}
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) {
- context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
+ // if cross signing is enabled we should propose full 4S
+ sessionHolder.getSafeActiveSession()?.let { session ->
+ if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) {
+ BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
+ } else {
+ context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
+ }
+ }
}
override fun openKeysBackupManager(context: Context) {
@@ -216,7 +243,8 @@ class DefaultNavigator @Inject constructor(
?.let { avatarUrl ->
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
val options = sharedElement?.let {
- ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
+ ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
+ ?: "")
}
activity.startActivity(intent, options?.toBundle())
}
@@ -244,27 +272,32 @@ class DefaultNavigator @Inject constructor(
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
}
- override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) {
- val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
- val pairs = ArrayList>()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ override fun openMediaViewer(activity: Activity,
+ roomId: String,
+ mediaData: AttachmentData,
+ view: View,
+ inMemory: List,
+ options: ((MutableList>) -> Unit)?) {
+ VectorAttachmentViewerActivity.newIntent(activity,
+ mediaData,
+ roomId,
+ mediaData.eventId,
+ inMemory,
+ ViewCompat.getTransitionName(view)).let { intent ->
+ val pairs = ArrayList>()
activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
+
+ pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
+ options?.invoke(pairs)
+
+ val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
+ activity.startActivity(intent, bundle)
}
- pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
- options?.invoke(pairs)
-
- val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
- activity.startActivity(intent, bundle)
- }
-
- override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
- val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
- activity.startActivity(intent)
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
index ce4d5ef3ea..2d817183be 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
@@ -22,12 +22,12 @@ import android.view.View
import androidx.core.util.Pair
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
+import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService
-import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
-import im.vector.riotx.features.media.ImageContentRenderer
-import im.vector.riotx.features.media.VideoContentRenderer
+import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
@@ -40,6 +40,8 @@ interface Navigator {
fun requestSessionVerification(context: Context, otherSessionId: String)
+ fun requestSelfSessionVerification(context: Context)
+
fun waitSessionVerification(context: Context)
fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean)
@@ -48,7 +50,7 @@ interface Navigator {
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
- fun openRoomPreview(publicRoom: PublicRoom, context: Context)
+ fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData)
fun openCreateRoom(context: Context, initialName: String = "")
@@ -91,7 +93,10 @@ interface Navigator {
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
- fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?)
-
- fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
+ fun openMediaViewer(activity: Activity,
+ roomId: String,
+ mediaData: AttachmentData,
+ view: View,
+ inMemory: List = emptyList(),
+ options: ((MutableList