~evilham/unchat-android

67057bfac4998d8dee8b88985da4d66f101b04a0 — Valere 3 years ago c0a83d1 + bcd8697
Merge pull request #2417 from vector-im/feature/bca/quick_invite_dm_tab

Feature/bca/quick invite dm tab
85 files changed, 2432 insertions(+), 651 deletions(-)

M AUTHORS.md
M CHANGES.md
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
M vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
M vector/src/debug/res/layout/activity_debug_menu.xml
M vector/src/main/AndroidManifest.xml
M vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
M vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt
M vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt
A vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt
M vector/src/main/java/im/vector/app/core/extensions/EditText.kt
M vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
M vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
M vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt
M vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt
M vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt
M vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
A vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt
M vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt
M vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt
M vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt
M vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt
M vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
R vector/src/main/java/im/vector/app/features/home/room/list/widget/{FabMenuView.kt => NotifsFabMenuView.kt}
M vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt
M vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt
A vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt
M vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt
M vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt
M vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt
M vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
M vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt
A vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt
A vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt
A vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt
A vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt
A vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt
A vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt
A vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt
A vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt
A vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt
A vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt
A vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt
D vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt
D vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt
R vector/src/main/java/im/vector/app/features/userdirectory/{UserDirectoryAction.kt => UserListAction.kt}
R vector/src/main/java/im/vector/app/features/userdirectory/{DirectoryUsersController.kt => UserListController.kt}
R vector/src/main/java/im/vector/app/features/userdirectory/{KnownUsersFragment.kt => UserListFragment.kt}
R vector/src/main/java/im/vector/app/features/userdirectory/{KnownUsersFragmentArgs.kt => UserListFragmentArgs.kt}
A vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt
R vector/src/main/java/im/vector/app/features/userdirectory/{UserDirectorySharedAction.kt => UserListSharedAction.kt}
R vector/src/main/java/im/vector/app/features/userdirectory/{UserDirectorySharedActionViewModel.kt => UserListSharedActionViewModel.kt}
R vector/src/main/java/im/vector/app/features/userdirectory/{UserDirectoryViewEvents.kt => UserListViewEvents.kt}
R vector/src/main/java/im/vector/app/features/userdirectory/{UserDirectoryViewModel.kt => UserListViewModel.kt}
R vector/src/main/java/im/vector/app/features/userdirectory/{UserDirectoryViewState.kt => UserListViewState.kt}
A vector/src/main/res/drawable/ic_baseline_perm_contact_calendar_24.xml
A vector/src/main/res/drawable/ic_book.xml
A vector/src/main/res/drawable/ic_fab_add_by_mxid.xml
A vector/src/main/res/drawable/ic_fab_add_by_qr_code.xml
A vector/src/main/res/drawable/ic_invite_people.xml
A vector/src/main/res/drawable/ic_picture_icon.xml
A vector/src/main/res/drawable/ic_qr_code_add.xml
M vector/src/main/res/layout/activity.xml
M vector/src/main/res/layout/activity_simple.xml
A vector/src/main/res/layout/bottom_sheet_matrix_to_card.xml
A vector/src/main/res/layout/dialog_share_qr_code.xml
M vector/src/main/res/layout/fragment_home_drawer.xml
M vector/src/main/res/layout/fragment_qr_code_scanner.xml
A vector/src/main/res/layout/fragment_qr_code_scanner_with_button.xml
M vector/src/main/res/layout/fragment_room_list.xml
A vector/src/main/res/layout/fragment_user_code_show.xml
R vector/src/main/res/layout/{fragment_known_users.xml => fragment_user_list.xml}
A vector/src/main/res/layout/item_checkbox.xml
A vector/src/main/res/layout/item_contact_action.xml
A vector/src/main/res/layout/item_user_list_header.xml
M vector/src/main/res/layout/item_verification_qr_code.xml
R vector/src/main/res/layout/{motion_fab_menu_merge.xml => motion_notifs_fab_menu_merge.xml}
M vector/src/main/res/values/strings.xml
M vector/src/main/res/values/style_snackbar.xml
R vector/src/main/res/xml/{motion_scene_fab_menu.xml => motion_scene_notifs_fab_menu.xml}
M AUTHORS.md => AUTHORS.md +4 -4
@@ 39,7 39,7 @@ We do not forget all translators, for their work of translating Element into man

Feel free to add your name below, when you contribute to the project!

Name    | Matrix ID           | GitHub
--------|---------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)

Name      | Matrix ID                   | GitHub
----------|-----------------------------|--------------------------------------
gjpower   | @gjpower:matrix.org         | [gjpower](https://github.com/gjpower)
TR_SLimey | @tr_slimey:an-atom-in.space | [TR-SLimey](https://github.com/TR-SLimey)

M CHANGES.md => CHANGES.md +4 -1
@@ 2,7 2,9 @@ Changes in Element 1.0.11 (2020-XX-XX)
===================================================

Features ✨:
 -
 - Create DMs with users by scanning their QR code (#2025)
 - Add Invite friends quick invite actions (#2348)
 - Add friend by scanning QR code, show your code to friends (#2025)

Improvements 🙌:
 - New room creation tile with quick action (#2346)


@@ 12,6 14,7 @@ Improvements 🙌:
 - Handle events of type "m.room.server_acl" (#890)
 - Room creation form: add advanced section to disable federation (#1314)
 - Move "Enable Encryption" from room setting screen to room profile screen (#2394)
 - Improve Invite user screen (seamless search for matrix ID)

Bugfix 🐛:
 - Fix crash on AttachmentViewer (#2365)

M matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +1 -1
@@ 27,7 27,7 @@ interface LoginWizard {
     * @param password the password field
     * @param deviceName the initial device name
     * @param callback  the matrix callback on which you'll receive the result of authentication.
     * @return return a [Cancelable]
     * @return a [Cancelable]
     */
    fun login(login: String,
              password: String,

M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +1 -1
@@ 1204,7 1204,7 @@ internal class DefaultVerificationService @Inject constructor(
        Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")

        val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
                ?.values?.map { it.deviceId } ?: emptyList()
                ?.values?.map { it.deviceId }.orEmpty()

        val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }


M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +1 -1
@@ 103,7 103,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
                    .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
                    .findAll()
                    ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
                    ?: emptyList()
                    .orEmpty()
        }
    }
}

M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt +1 -1
@@ 28,7 28,7 @@ internal class RoomTypingUsersHandler @Inject constructor(@UserId private val us

    fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
        val roomMemberHelper = RoomMemberHelper(realm, roomId)
        val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId } ?: emptyList()
        val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty()
        val senderInfo = typingIds.map { userId ->
            val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
            SenderInfo(

M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt +1 -1
@@ 37,6 37,6 @@ internal class DefaultTypingUsersTracker @Inject constructor() : TypingUsersTrac
    }

    override fun getTypingUsers(roomId: String): List<SenderInfo> {
        return typingUsers[roomId] ?: emptyList()
        return typingUsers[roomId].orEmpty()
    }
}

M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +1 -1
@@ 138,7 138,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
    ): LiveData<List<Widget>> {
        val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS)
        return Transformations.map(widgetsAccountData) {
            it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList()
            it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes).orEmpty()
        }
    }


M vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt => vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +4 -1
@@ 310,7 310,10 @@ class UiAllScreensSanityTest {
        clickOn(R.id.createChatRoomButton)

        withIdlingResource(activityIdlingResource(CreateDirectRoomActivity::class.java)) {
            assertDisplayed(R.id.addByMatrixId)
            onView(withId(R.id.userListRecyclerView))
                    .perform(waitForView(withText(R.string.qr_code)))
            onView(withId(R.id.userListRecyclerView))
                    .perform(waitForView(withText(R.string.invite_friends)))
        }

        closeSoftKeyboard()

M vector/src/debug/res/layout/activity_debug_menu.xml => vector/src/debug/res/layout/activity_debug_menu.xml +1 -1
@@ 72,7 72,7 @@
                android:id="@+id/debug_qr_code"
                android:layout_width="200dp"
                android:layout_height="200dp"
                tools:src="@tools:sample/avatars" />
                tools:src="@drawable/ic_qr_code_add" />

        </LinearLayout>


M vector/src/main/AndroidManifest.xml => vector/src/main/AndroidManifest.xml +1 -0
@@ 229,6 229,7 @@
        <activity android:name=".features.widgets.WidgetActivity" />
        <activity android:name=".features.pin.PinActivity" />
        <activity android:name=".features.home.room.detail.search.SearchActivity" />
        <activity android:name=".features.usercode.UserCodeActivity" />

        <!-- Services -->


M vector/src/main/java/im/vector/app/core/di/FragmentModule.kt => vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +9 -9
@@ 111,8 111,8 @@ import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
import im.vector.app.features.share.IncomingShareFragment
import im.vector.app.features.signout.soft.SoftLogoutFragment
import im.vector.app.features.terms.ReviewTermsFragment
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.usercode.ShowUserCodeFragment
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.widgets.WidgetFragment

@Module


@@ 255,13 255,8 @@ interface FragmentModule {

    @Binds
    @IntoMap
    @FragmentKey(UserDirectoryFragment::class)
    fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment

    @Binds
    @IntoMap
    @FragmentKey(KnownUsersFragment::class)
    fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment
    @FragmentKey(UserListFragment::class)
    fun bindUserListFragment(fragment: UserListFragment): Fragment

    @Binds
    @IntoMap


@@ 582,4 577,9 @@ interface FragmentModule {
    @IntoMap
    @FragmentKey(SearchFragment::class)
    fun bindSearchFragment(fragment: SearchFragment): Fragment

    @Binds
    @IntoMap
    @FragmentKey(ShowUserCodeFragment::class)
    fun bindShowUserCodeFragment(fragment: ShowUserCodeFragment): Fragment
}

M vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt => vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +4 -0
@@ 50,6 50,7 @@ import im.vector.app.features.invite.InviteUsersToRoomActivity
import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.link.LinkHandlerActivity
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.media.BigImageViewerActivity
import im.vector.app.features.media.VectorAttachmentViewerActivity
import im.vector.app.features.navigation.Navigator


@@ 72,6 73,7 @@ import im.vector.app.features.share.IncomingShareActivity
import im.vector.app.features.signout.soft.SoftLogoutActivity
import im.vector.app.features.terms.ReviewTermsActivity
import im.vector.app.features.ui.UiStateRepository
import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.widgets.WidgetActivity
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment


@@ 140,6 142,7 @@ interface ScreenComponent {
    fun inject(activity: VectorAttachmentViewerActivity)
    fun inject(activity: VectorJitsiActivity)
    fun inject(activity: SearchActivity)
    fun inject(activity: UserCodeActivity)

    /* ==========================================================================================
     * BottomSheets


@@ 158,6 161,7 @@ interface ScreenComponent {
    fun inject(bottomSheet: RoomWidgetsBottomSheet)
    fun inject(bottomSheet: CallControlsBottomSheet)
    fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
    fun inject(bottomSheet: MatrixToBottomSheet)

    /* ==========================================================================================
     * Others

M vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt => vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +3 -3
@@ 35,7 35,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.reactions.EmojiChooserViewModel
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserListSharedActionViewModel

@Module
interface ViewModelModule {


@@ 87,8 87,8 @@ interface ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(UserDirectorySharedActionViewModel::class)
    fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel
    @ViewModelKey(UserListSharedActionViewModel::class)
    fun bindUserListSharedActionViewModel(viewModel: UserListSharedActionViewModel): ViewModel

    @Binds
    @IntoMap

A vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt => vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt +46 -0
@@ 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.app.core.epoxy

import android.widget.CompoundButton
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.checkbox.MaterialCheckBox
import im.vector.app.R

@EpoxyModelClass(layout = R.layout.item_checkbox)
abstract class CheckBoxItem : VectorEpoxyModel<CheckBoxItem.Holder>() {

    @EpoxyAttribute
    var checked: Boolean = false

    @EpoxyAttribute lateinit var title: String

    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
    var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null

    override fun bind(holder: Holder) {
        super.bind(holder)
        holder.checkbox.isChecked = checked
        holder.checkbox.text = title
        holder.checkbox.setOnCheckedChangeListener(checkChangeListener)
    }

    class Holder : VectorEpoxyHolder() {
        val checkbox by bind<MaterialCheckBox>(R.id.checkbox)
    }
}

M vector/src/main/java/im/vector/app/core/extensions/EditText.kt => vector/src/main/java/im/vector/app/core/extensions/EditText.kt +1 -1
@@ 26,7 26,7 @@ import androidx.annotation.DrawableRes
import im.vector.app.R
import im.vector.app.core.platform.SimpleTextWatcher

fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter,
fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_search,
                           @DrawableRes clearIconRes: Int = R.drawable.ic_x_gray) {
    addTextChangedListener(object : SimpleTextWatcher() {
        override fun afterTextChanged(s: Editable) {

M vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt => vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +10 -0
@@ 587,6 587,16 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
        }
    }

    fun showSnackbar(message: String, @StringRes withActionTitle: Int?, action: (() -> Unit)?) {
        coordinatorLayout?.let {
            Snackbar.make(it, message, Snackbar.LENGTH_LONG).apply {
                withActionTitle?.let {
                    setAction(withActionTitle, { action?.invoke() })
                }
            }.show()
        }
    }

    /* ==========================================================================================
     * User Consent
     * ========================================================================================== */

M vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt => vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +14 -0
@@ 29,6 29,7 @@ import android.os.Build
import android.os.Environment
import android.provider.Browser
import android.provider.MediaStore
import android.provider.Settings
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher


@@ 448,6 449,19 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
    }
}

fun openAppSettingsPage(activity: Activity) {
    try {
        activity.startActivity(
                Intent().apply {
                    action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    data = Uri.fromParts("package", activity.packageName, null)
                })
    } catch (activityNotFoundException: ActivityNotFoundException) {
        activity.toast(R.string.error_no_external_application_found)
    }
}

/**
 * Ask the user to select a location and a file name to write in
 */

M vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt => vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +7 -0
@@ 30,6 30,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity
import timber.log.Timber

// Android M permission request code management


@@ 284,6 285,12 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
    return isPermissionGranted
}

fun VectorBaseActivity.onPermissionDeniedSnackbar(@StringRes rationaleMessage: Int) {
    showSnackbar(getString(rationaleMessage), R.string.settings) {
        openAppSettingsPage(this)
    }
}

/**
 * Helper method used in [.checkPermissions] to populate the list of the
 * permissions to be granted (permissionsListToBeGrantedOut) and the list of the permissions already denied (permissionAlreadyDeniedListOut).

M vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt => vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +7 -1
@@ 136,13 136,19 @@ fun startSharePlainTextIntent(fragment: Fragment,
                              activityResultLauncher: ActivityResultLauncher<Intent>?,
                              chooserTitle: String?,
                              text: String,
                              subject: String? = null) {
                              subject: String? = null,
                              extraTitle: String? = null) {
    val share = Intent(Intent.ACTION_SEND)
    share.type = "text/plain"
    share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
    // Add data to the intent, the receiving app will decide what to do with it.
    share.putExtra(Intent.EXTRA_SUBJECT, subject)
    share.putExtra(Intent.EXTRA_TEXT, text)

    extraTitle?.let {
        share.putExtra(Intent.EXTRA_TITLE, it)
    }

    val intent = Intent.createChooser(share, chooserTitle)
    try {
        if (activityResultLauncher != null) {

M vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt => vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +12 -12
@@ 30,10 30,10 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.UserDirectoryAction
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListAction
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import kotlinx.android.synthetic.main.fragment_contacts_book.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User


@@ 46,16 46,16 @@ class ContactsBookFragment @Inject constructor(
) : VectorBaseFragment(), ContactsBookController.Callback {

    override fun getLayoutResId() = R.layout.fragment_contacts_book
    private val viewModel: UserDirectoryViewModel by activityViewModel()
    private val viewModel: UserListViewModel by activityViewModel()

    // Use activityViewModel to avoid loading several times the data
    private val contactsBookViewModel: ContactsBookViewModel by activityViewModel()

    private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
    private lateinit var sharedActionViewModel: UserListSharedActionViewModel

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
        sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
        setupRecyclerView()
        setupFilterView()
        setupConsentView()


@@ 110,7 110,7 @@ class ContactsBookFragment @Inject constructor(

    private fun setupCloseView() {
        phoneBookClose.debouncedClicks {
            sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
            sharedActionViewModel.post(UserListSharedAction.GoBack)
        }
    }



@@ 122,13 122,13 @@ class ContactsBookFragment @Inject constructor(

    override fun onMatrixIdClick(matrixId: String) {
        view?.hideKeyboard()
        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
        sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
        viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
        sharedActionViewModel.post(UserListSharedAction.GoBack)
    }

    override fun onThreePidClick(threePid: ThreePid) {
        view?.hideKeyboard()
        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
        sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
        viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
        sharedActionViewModel.post(UserListSharedAction.GoBack)
    }
}

M vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt => vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +45 -24
@@ 37,28 37,31 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.KnownUsersFragmentArgs
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState
import kotlinx.android.synthetic.main.activity.*
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import java.net.HttpURLConnection
import javax.inject.Inject

class CreateDirectRoomActivity : SimpleFragmentActivity() {
class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {

    private val viewModel: CreateDirectRoomViewModel by viewModel()
    private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
    @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
    private lateinit var sharedActionViewModel: UserListSharedActionViewModel
    @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
    @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
    @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
    @Inject lateinit var errorFormatter: ErrorFormatter


@@ 68,31 71,34 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
        injector.inject(this)
    }

    override fun create(initialState: UserListViewState): UserListViewModel {
        return userListViewModelFactory.create(initialState)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        toolbar.visibility = View.GONE
        sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)

        sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
        sharedActionViewModel
                .observe()
                .subscribe { sharedAction ->
                    when (sharedAction) {
                        UserDirectorySharedAction.OpenUsersDirectory    ->
                            addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
                        UserDirectorySharedAction.Close                 -> finish()
                        UserDirectorySharedAction.GoBack                -> onBackPressed()
                        is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
                        UserDirectorySharedAction.OpenPhoneBook         -> openPhoneBook()
                .subscribe { action ->
                    when (action) {
                        UserListSharedAction.Close                 -> finish()
                        UserListSharedAction.GoBack                -> onBackPressed()
                        is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(action)
                        UserListSharedAction.OpenPhoneBook         -> openPhoneBook()
                        UserListSharedAction.AddByQrCode           -> openAddByQrCode()
                    }.exhaustive
                }
                .disposeOnDestroy()
        if (isFirstCreation()) {
            addFragment(
                    R.id.container,
                    KnownUsersFragment::class.java,
                    KnownUsersFragmentArgs(
                    UserListFragment::class.java,
                    UserListFragmentArgs(
                            title = getString(R.string.fab_menu_create_chat),
                            menuResId = R.menu.vector_create_direct_room,
                            isCreatingRoom = true
                            menuResId = R.menu.vector_create_direct_room
                    )
            )
        }


@@ 101,6 107,12 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
        }
    }

    private fun openAddByQrCode() {
        if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA, 0)) {
            addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
        }
    }

    private fun openPhoneBook() {
        // Check permission first
        if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,


@@ 116,15 128,23 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
        if (allGranted(grantResults)) {
            if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
                doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
            } else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
                addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
            }
        } else {
            if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
                onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
            } else if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
                onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact)
            }
        }
    }

    private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
    private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
        if (action.itemId == R.id.action_create_direct_room) {
            viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(
                    action.invitees,
                    action.existingDmRoomId
                    null
            ))
        }
    }


@@ 178,6 198,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
    }

    companion object {

        fun getIntent(context: Context): Intent {
            return Intent(context, CreateDirectRoomActivity::class.java)
        }

A vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt => vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +122 -0
@@ 0,0 1,122 @@
/*
 * 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.app.features.createdirect

import android.widget.Toast
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.features.userdirectory.PendingInvitee
import kotlinx.android.synthetic.main.fragment_qr_code_scanner.*
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject

class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragment(), ZXingScannerView.ResultHandler {

    private val viewModel: CreateDirectRoomViewModel by activityViewModel()

    override fun getLayoutResId() = R.layout.fragment_qr_code_scanner

    private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
        if (allGranted) {
            startCamera()
        }
    }

    private fun startCamera() {
        // Start camera on resume
        scannerView.startCamera()
    }

    override fun onResume() {
        super.onResume()
        view?.hideKeyboard()
        // Register ourselves as a handler for scan results.
        scannerView.setResultHandler(this)
        // Start camera on resume
        if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
            startCamera()
        }
    }

    override fun onPause() {
        super.onPause()
        // Unregister ourselves as a handler for scan results.
        scannerView.setResultHandler(null)
        // Stop camera on pause
        scannerView.stopCamera()
    }

    // Copied from https://github.com/markusfisch/BinaryEye/blob/
    // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
    private fun getRawBytes(result: Result): ByteArray? {
        val metadata = result.resultMetadata ?: return null
        val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
        var bytes = ByteArray(0)
        @Suppress("UNCHECKED_CAST")
        for (seg in segments as Iterable<ByteArray>) {
            bytes += seg
        }
        // byte segments can never be shorter than the text.
        // Zxing cuts off content prefixes like "WIFI:"
        return if (bytes.size >= result.text.length) bytes else null
    }

    private fun addByQrCode(value: String) {
        val mxid = (PermalinkParser.parse(value) as? PermalinkData.UserLink)?.userId

        if (mxid === null) {
            Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show()
            requireActivity().finish()
        } else {
            val existingDm = viewModel.session.getExistingDirectRoomWithUser(mxid)
            // The following assumes MXIDs are case insensitive
            if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) {
                Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
                requireActivity().finish()
            } else {
                // Try to get user from known users and fall back to creating a User object from MXID
                val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)

                viewModel.handle(
                        CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm)
                )
            }
        }
    }

    override fun handleResult(result: Result?) {
        if (result === null) {
            Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
            requireActivity().finish()
        } else {
            val rawBytes = getRawBytes(result)
            val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
            val value = rawBytesStr ?: result.text
            addByQrCode(value)
        }
    }
}

M vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt => vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +1 -1
@@ 38,7 38,7 @@ import org.matrix.android.sdk.rx.rx
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
                                                            initialState: CreateDirectRoomViewState,
                                                            private val rawService: RawService,
                                                            private val session: Session)
                                                            val session: Session)
    : VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {

    @AssistedInject.Factory

M vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt => vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +30 -0
@@ 18,15 18,19 @@ package im.vector.app.features.home

import android.os.Bundle
import android.view.View
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.observeK
import im.vector.app.core.extensions.replaceChildFragment
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.grouplist.GroupListFragment
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.workers.signout.SignOutUiWorker
import kotlinx.android.synthetic.main.fragment_home_drawer.*
import org.matrix.android.sdk.api.session.Session


@@ 75,6 79,32 @@ class HomeDrawerFragment @Inject constructor(
            SignOutUiWorker(requireActivity()).perform()
        }

        homeDrawerQRCodeButton.debouncedClicks {
            UserCodeActivity.newIntent(requireContext(), sharedActionViewModel.session.myUserId).let {
                val options =
                        ActivityOptionsCompat.makeSceneTransitionAnimation(
                                requireActivity(),
                                homeDrawerHeaderAvatarView,
                                ViewCompat.getTransitionName(homeDrawerHeaderAvatarView) ?: ""
                        )
                startActivity(it, options.toBundle())
            }
        }

        homeDrawerInviteFriendButton.debouncedClicks {
            session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
                val text = getString(R.string.invite_friends_text, permalink)

                startSharePlainTextIntent(
                        fragment = this,
                        activityResultLauncher = null,
                        chooserTitle = getString(R.string.invite_friends),
                        text = text,
                        extraTitle = getString(R.string.invite_friends_rich_title)
                )
            }
        }

        // Debug menu
        homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode()
        homeDrawerHeaderDebugView.debouncedClicks {

M vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt => vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt +2 -1
@@ 17,6 17,7 @@
package im.vector.app.features.home

import im.vector.app.core.platform.VectorSharedActionViewModel
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject

class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>()
class HomeSharedActionViewModel @Inject constructor(val session: Session) : VectorSharedActionViewModel<HomeActivitySharedAction>()

M vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt => vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt +2 -2
@@ 22,7 22,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.room.list.widget.FabMenuView
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView

@EpoxyModelClass(layout = R.layout.item_room_filter_footer)
abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.Holder>() {


@@ 46,7 46,7 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.
        val openRoomDirectory by bind<Button>(R.id.roomFilterFooterOpenRoomDirectory)
    }

    interface FilteredRoomFooterItemListener : FabMenuView.Listener {
    interface FilteredRoomFooterItemListener : NotifsFabMenuView.Listener {
        fun createRoom(initialName: String)
    }
}

M vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt => vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +2 -3
@@ 45,7 45,7 @@ import im.vector.app.features.home.room.list.actions.RoomListActionsArgs
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.home.room.list.widget.FabMenuView
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.notifications.NotificationDrawerManager
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.*


@@ 66,8 66,7 @@ class RoomListFragment @Inject constructor(
        val roomListViewModelFactory: RoomListViewModel.Factory,
        private val notificationDrawerManager: NotificationDrawerManager,
        private val sharedViewPool: RecyclerView.RecycledViewPool

) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, NotifsFabMenuView.Listener {

    private var modelBuildListener: OnModelBuildFinishedListener? = null
    private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel

R vector/src/main/java/im/vector/app/features/home/room/list/widget/FabMenuView.kt => vector/src/main/java/im/vector/app/features/home/room/list/widget/NotifsFabMenuView.kt +4 -4
@@ 22,15 22,15 @@ import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.isVisible
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.app.R
import kotlinx.android.synthetic.main.motion_fab_menu_merge.view.*
import kotlinx.android.synthetic.main.motion_notifs_fab_menu_merge.view.*

class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
                                            defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
class NotifsFabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
                                                  defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {

    var listener: Listener? = null

    init {
        inflate(context, R.layout.motion_fab_menu_merge, this)
        inflate(context, R.layout.motion_notifs_fab_menu_merge, this)
    }

    override fun onFinishInflate() {

M vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt => vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt +2 -2
@@ 28,7 28,7 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.UserListFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull


@@ 50,7 50,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor(
    companion object : MvRxViewModelFactory<HomeServerCapabilitiesViewModel, HomeServerCapabilitiesViewState> {
        @JvmStatic
        override fun create(viewModelContext: ViewModelContext, state: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel? {
            val fragment: KnownUsersFragment = (viewModelContext as FragmentViewModelContext).fragment()
            val fragment: UserListFragment = (viewModelContext as FragmentViewModelContext).fragment()
            return fragment.homeServerCapabilitiesViewModelFactory.create(state)
        }


M vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt => vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +37 -27
@@ 21,6 21,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel


@@ 29,7 30,6 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH


@@ 39,12 39,12 @@ import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.toast
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.KnownUsersFragmentArgs
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.*
import org.matrix.android.sdk.api.failure.Failure


@@ 54,11 54,11 @@ import javax.inject.Inject
@Parcelize
data class InviteUsersToRoomArgs(val roomId: String) : Parcelable

class InviteUsersToRoomActivity : SimpleFragmentActivity() {
class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {

    private val viewModel: InviteUsersToRoomViewModel by viewModel()
    private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
    @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
    private lateinit var sharedActionViewModel: UserListSharedActionViewModel
    @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
    @Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
    @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
    @Inject lateinit var errorFormatter: ErrorFormatter


@@ 68,32 68,40 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
        injector.inject(this)
    }

    override fun create(initialState: UserListViewState): UserListViewModel {
        return userListViewModelFactory.create(initialState)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        toolbar.visibility = View.GONE
        sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)

        sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
        sharedActionViewModel
                .observe()
                .subscribe { sharedAction ->
                    when (sharedAction) {
                        UserDirectorySharedAction.OpenUsersDirectory    ->
                            addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
                        UserDirectorySharedAction.Close                 -> finish()
                        UserDirectorySharedAction.GoBack                -> onBackPressed()
                        is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
                        UserDirectorySharedAction.OpenPhoneBook         -> openPhoneBook()
                    }.exhaustive
                        UserListSharedAction.Close                 -> finish()
                        UserListSharedAction.GoBack                -> onBackPressed()
                        is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
                        UserListSharedAction.OpenPhoneBook         -> openPhoneBook()
                        // not exhaustive because it's a sharedAction
                        else                                       -> {
                        }
                    }
                }
                .disposeOnDestroy()
        if (isFirstCreation()) {
            val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG)
            addFragment(
                    R.id.container,
                    KnownUsersFragment::class.java,
                    KnownUsersFragmentArgs(
                    UserListFragment::class.java,
                    UserListFragmentArgs(
                            title = getString(R.string.invite_users_to_room_title),
                            menuResId = R.menu.vector_invite_users_to_room,
                            excludedUserIds = viewModel.getUserIdsOfRoomMembers()
                            excludedUserIds = viewModel.getUserIdsOfRoomMembers(),
                            existingRoomId = args?.roomId
                    )
            )
        }


@@ 101,6 109,12 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
        viewModel.observeViewEvents { renderInviteEvents(it) }
    }

    private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
        if (action.itemId == R.id.action_invite_users_to_room_invite) {
            viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
        }
    }

    private fun openPhoneBook() {
        // Check permission first
        if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,


@@ 117,12 131,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
            if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
                doOnPostResume { 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.invitees))
        } else {
            Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
        }
    }


A vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt => vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +65 -0
@@ 0,0 1,65 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.matrixto

import android.os.Bundle
import android.view.View
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject

class MatrixToBottomSheet(private val matrixItem: MatrixItem) : VectorBaseBottomSheetDialogFragment() {

    @Inject lateinit var avatarRenderer: AvatarRenderer

    interface InteractionListener {
        fun didTapStartMessage(matrixItem: MatrixItem)
    }

    override fun injectWith(injector: ScreenComponent) {
        injector.inject(this)
    }

    private var interactionListener: InteractionListener? = null

    override fun getLayoutResId() = R.layout.bottom_sheet_matrix_to_card

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        matrixToCardSendMessageButton.debouncedClicks {
            interactionListener?.didTapStartMessage(matrixItem)
            dismiss()
        }

        matrixToCardNameText.setTextOrHide(matrixItem.displayName)
        matrixToCardUserIdText.setTextOrHide(matrixItem.id)
        avatarRenderer.render(matrixItem, matrixToCardAvatar)
    }

    companion object {
        fun create(matrixItem: MatrixItem, listener: InteractionListener?): MatrixToBottomSheet {
            return MatrixToBottomSheet(matrixItem).apply {
                interactionListener = listener
            }
        }
    }
}

M vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt => vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +3 -3
@@ 204,9 204,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
            Timber.w("Try to join an already joining room. Should not happen")
            return@withState
        }
        val viaServers = state.roomDirectoryData.homeServer?.let {
            listOf(it)
        } ?: emptyList()
        val viaServers = state.roomDirectoryData.homeServer
                ?.let { listOf(it) }
                .orEmpty()
        session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback<Unit> {
            override fun onSuccess(data: Unit) {
                // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.

M vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt => vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt +2 -2
@@ 62,7 62,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>() 
        holder.avatarView.isInvisible = directoryAvatarUrl.isNullOrBlank() && includeAllNetworks

        holder.nameView.text = directoryName
        holder.descritionView.setTextOrHide(directoryDescription)
        holder.descriptionView.setTextOrHide(directoryDescription)
    }

    class Holder : VectorEpoxyHolder() {


@@ 70,6 70,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>() 

        val avatarView by bind<ImageView>(R.id.itemRoomDirectoryAvatar)
        val nameView by bind<TextView>(R.id.itemRoomDirectoryName)
        val descritionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
        val descriptionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
    }
}

M vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt => vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +11 -0
@@ 79,6 79,17 @@ class RoomMemberProfileController @Inject constructor(
                divider = false,
                action = { callback?.onIgnoreClicked() }
        )
        if (!state.isMine) {
            buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))

            buildProfileAction(
                    id = "direct",
                    editable = false,
                    title = stringProvider.getString(R.string.room_member_open_or_create_dm),
                    dividerColor = dividerColor,
                    action = { callback?.onOpenDmClicked() }
            )
        }
    }

    private fun buildRoomMemberActions(state: RoomMemberProfileViewState) {

M vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt => vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +14 -6
@@ 294,12 294,20 @@ class RoomMemberProfileFragment @Inject constructor(
    }

    private fun handleShareRoomMemberProfile(permalink: String) {
        startSharePlainTextIntent(
                fragment = this,
                activityResultLauncher = null,
                chooserTitle = null,
                text = permalink
        )
        val view = layoutInflater.inflate(R.layout.dialog_share_qr_code, null)
        val qrCode = view.findViewById<im.vector.app.core.ui.views.QrCodeImageView>(R.id.itemShareQrCodeImage)
        qrCode.setData(permalink)
        AlertDialog.Builder(requireContext())
            .setView(view)
            .setNeutralButton(R.string.ok, null)
            .setPositiveButton(R.string.share_by_text) { _, _ ->
                startSharePlainTextIntent(
                        fragment = this,
                        activityResultLauncher = null,
                        chooserTitle = null,
                        text = permalink
                )
            }.show()
    }

    private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) {

M vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt => vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +2 -15
@@ 16,15 16,13 @@

package im.vector.app.features.settings

import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.preference.Preference
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.displayInWebView
import im.vector.app.core.utils.openAppSettingsPage
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.features.version.VersionProvider
import im.vector.app.openOssLicensesMenuActivity


@@ 42,18 40,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
        // preference to start the App info screen, to facilitate App permissions access
        findPreference<VectorPreference>(APP_INFO_LINK_PREFERENCE_KEY)!!
                .onPreferenceClickListener = Preference.OnPreferenceClickListener {
            activity?.let {
                val intent = Intent().apply {
                    action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

                    val uri = Uri.fromParts("package", requireContext().packageName, null)

                    data = uri
                }
                it.applicationContext.startActivity(intent)
            }

            activity?.let { openAppSettingsPage(it) }
            true
        }


A vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt => vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt +85 -0
@@ 0,0 1,85 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.usercode

import android.graphics.Bitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.LuminanceSource
import com.google.zxing.MultiFormatReader
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.ReaderException
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer

// Some helper code from BinaryEye
object QRCodeBitmapDecodeHelper {

    private val multiFormatReader = MultiFormatReader()
    private val decoderHints = mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))

    fun decodeQRFromBitmap(bitmap: Bitmap): Result? =
            decode(bitmap, false) ?: decode(bitmap, true)

    private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
        val pixels = IntArray(bitmap.width * bitmap.height)
        return decode(pixels, bitmap, invert)
    }

    private fun decode(
            pixels: IntArray,
            bitmap: Bitmap,
            invert: Boolean = false
    ): Result? {
        val width = bitmap.width
        val height = bitmap.height
        if (bitmap.config != Bitmap.Config.ARGB_8888) {
            bitmap.copy(Bitmap.Config.ARGB_8888, true)
        } else {
            bitmap
        }.getPixels(pixels, 0, width, 0, 0, width, height)
        return decodeLuminanceSource(
                RGBLuminanceSource(width, height, pixels),
                invert
        )
    }

    private fun decodeLuminanceSource(
            source: LuminanceSource,
            invert: Boolean
    ): Result? {
        return decodeLuminanceSource(
                if (invert) {
                    source.invert()
                } else {
                    source
                }
        )
    }

    private fun decodeLuminanceSource(source: LuminanceSource): Result? {
        val bitmap = BinaryBitmap(HybridBinarizer(source))
        return try {
            multiFormatReader.decode(bitmap, decoderHints)
        } catch (e: ReaderException) {
            null
        } finally {
            multiFormatReader.reset()
        }
    }
}

A vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt => vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt +148 -0
@@ 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.app.features.usercode

import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.utils.ImageUtils
import kotlinx.android.synthetic.main.fragment_qr_code_scanner_with_button.*
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject

class ScanUserCodeFragment @Inject constructor()
    : VectorBaseFragment(),
        ZXingScannerView.ResultHandler {

    override fun getLayoutResId() = R.layout.fragment_qr_code_scanner_with_button

    val sharedViewModel: UserCodeSharedViewModel by activityViewModel()

    var autoFocus = true

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        userCodeMyCodeButton.debouncedClicks {
            sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
        }

        userCodeOpenGalleryButton.debouncedClicks {
            MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
        }
    }

    private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
        if (allGranted) {
            startCamera()
        } else {
            // For now just go back
            sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
        }
    }

    private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
        if (activityResult.resultCode == Activity.RESULT_OK) {
            MultiPicker
                    .get(MultiPicker.IMAGE)
                    .getSelectedFiles(requireActivity(), activityResult.data)
                    .firstOrNull()
                    ?.contentUri
                    ?.let { uri ->
                        // try to see if it is a valid matrix code
                        val bitmap = ImageUtils.getBitmap(requireContext(), uri)
                                ?: return@let Unit.also {
                                    Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show()
                                }
                        handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) })
                    }
        }
    }

    private fun startCamera() {
        userCodeScannerView.startCamera()
        userCodeScannerView.setAutoFocus(autoFocus)
        userCodeScannerView.debouncedClicks {
            this.autoFocus = !autoFocus
            userCodeScannerView.setAutoFocus(autoFocus)
        }
    }

    override fun onStart() {
        super.onStart()
        if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
            startCamera()
        }
    }

    override fun onResume() {
        super.onResume()
        // Register ourselves as a handler for scan results.
        userCodeScannerView.setResultHandler(this)
        if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)) {
            startCamera()
        }
    }

    override fun onPause() {
        super.onPause()
        userCodeScannerView.setResultHandler(null)
        // Stop camera on pause
        userCodeScannerView.stopCamera()
    }

    override fun handleResult(result: Result?) {
        if (result === null) {
            Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
            requireActivity().finish()
        } else {
            val rawBytes = getRawBytes(result)
            val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
            val value = rawBytesStr ?: result.text
            sharedViewModel.handle(UserCodeActions.DecodedQRCode(value))
        }
    }

    // Copied from https://github.com/markusfisch/BinaryEye/blob/
    // 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
    private fun getRawBytes(result: Result): ByteArray? {
        val metadata = result.resultMetadata ?: return null
        val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
        var bytes = ByteArray(0)
        @Suppress("UNCHECKED_CAST")
        for (seg in segments as Iterable<ByteArray>) {
            bytes += seg
        }
        // byte segments can never be shorter than the text.
        // Zxing cuts off content prefixes like "WIFI:"
        return if (bytes.size >= result.text.length) bytes else null
    }
}

A vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt => vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt +87 -0
@@ 0,0 1,87 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.usercode

import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.fragment_user_code_show.*
import javax.inject.Inject

class ShowUserCodeFragment @Inject constructor(
        private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() {

    override fun getLayoutResId() = R.layout.fragment_user_code_show

    val sharedViewModel: UserCodeSharedViewModel by activityViewModel()

    private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
        if (allGranted) {
            doOpenQRCodeScanner()
        } else {
            sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted)
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        showUserCodeClose.debouncedClicks {
            sharedViewModel.handle(UserCodeActions.DismissAction)
        }
        showUserCodeScanButton.debouncedClicks {
            if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
                doOpenQRCodeScanner()
            }
        }
        showUserCodeShareButton.debouncedClicks {
            sharedViewModel.handle(UserCodeActions.ShareByText)
        }

        sharedViewModel.observeViewEvents {
            if (it is UserCodeShareViewEvents.SharePlainText) {
                startSharePlainTextIntent(
                        fragment = this,
                        activityResultLauncher = null,
                        chooserTitle = it.title,
                        text = it.text,
                        extraTitle = it.richPlainText
                )
            }
        }
    }

    private fun doOpenQRCodeScanner() {
        sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SCAN))
    }

    override fun invalidate() = withState(sharedViewModel) { state ->
        state.matrixItem?.let { avatarRenderer.render(it, showUserCodeAvatar) }
        state.shareLink?.let { showUserCodeQRImage.setData(it) }
        showUserCodeCardNameText.setTextOrHide(state.matrixItem?.displayName)
        showUserCodeCardUserIdText.setTextOrHide(state.matrixItem?.id)
    }
}

A vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt +29 -0
@@ 0,0 1,29 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.usercode

import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.util.MatrixItem

sealed class UserCodeActions : VectorViewModelAction {
    object DismissAction : UserCodeActions()
    data class SwitchMode(val mode: UserCodeState.Mode) : UserCodeActions()
    data class DecodedQRCode(val code: String) : UserCodeActions()
    data class StartChattingWithUser(val matrixItem: MatrixItem) : UserCodeActions()
    object CameraPermissionNotGranted : UserCodeActions()
    object ShareByText : UserCodeActions()
}

A vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +129 -0
@@ 0,0 1,129 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.usercode

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.matrixto.MatrixToBottomSheet
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_simple.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
import kotlin.reflect.KClass

class UserCodeActivity
    : VectorBaseActivity(), UserCodeSharedViewModel.Factory, MatrixToBottomSheet.InteractionListener {

    @Inject lateinit var viewModelFactory: UserCodeSharedViewModel.Factory

    val sharedViewModel: UserCodeSharedViewModel by viewModel()

    @Parcelize
    data class Args(
            val userId: String
    ) : Parcelable

    override fun getLayoutRes() = R.layout.activity_simple

    override fun injectWith(injector: ScreenComponent) {
        injector.inject(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (isFirstCreation()) {
            // should be there early for shared element transition
            showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
        }

        sharedViewModel.selectSubscribe(this, UserCodeState::mode) { mode ->
            when (mode) {
                UserCodeState.Mode.SHOW      -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
                UserCodeState.Mode.SCAN      -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY)
                is UserCodeState.Mode.RESULT -> {
                    showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
                    MatrixToBottomSheet.create(mode.matrixItem, this).show(supportFragmentManager, "MatrixToBottomSheet")
                }
            }
        }

        sharedViewModel.observeViewEvents {
            when (it) {
                UserCodeShareViewEvents.Dismiss                    -> ActivityCompat.finishAfterTransition(this)
                UserCodeShareViewEvents.ShowWaitingScreen          -> simpleActivityWaitingView.isVisible = true
                UserCodeShareViewEvents.HideWaitingScreen          -> simpleActivityWaitingView.isVisible = false
                is UserCodeShareViewEvents.ToastMessage            -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
                is UserCodeShareViewEvents.NavigateToRoom          -> navigator.openRoom(this, it.roomId)
                UserCodeShareViewEvents.CameraPermissionNotGranted -> onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
                else                                               -> {
                }
            }
        }
    }

    private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
        if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
            supportFragmentManager.commitTransaction {
                setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
                replace(R.id.simpleFragmentContainer,
                        fragmentClass.java,
                        bundle,
                        fragmentClass.simpleName
                )
            }
        }
    }

    override fun didTapStartMessage(matrixItem: MatrixItem) {
        sharedViewModel.handle(UserCodeActions.StartChattingWithUser(matrixItem))
    }

    override fun onBackPressed() = withState(sharedViewModel) {
        when (it.mode) {
            UserCodeState.Mode.SHOW -> super.onBackPressed()
            is UserCodeState.Mode.RESULT,
            UserCodeState.Mode.SCAN -> sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
        }.exhaustive
    }

    override fun create(initialState: UserCodeState) =
            viewModelFactory.create(initialState)

    companion object {
        fun newIntent(context: Context, userId: String): Intent {
            return Intent(context, UserCodeActivity::class.java).apply {
                putExtra(MvRx.KEY_ARG, Args(userId))
            }
        }
    }
}

A vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt +29 -0
@@ 0,0 1,29 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.usercode

import im.vector.app.core.platform.VectorViewEvents

sealed class UserCodeShareViewEvents : VectorViewEvents {
    object Dismiss : UserCodeShareViewEvents()
    object ShowWaitingScreen : UserCodeShareViewEvents()
    object HideWaitingScreen : UserCodeShareViewEvents()
    data class ToastMessage(val message: String) : UserCodeShareViewEvents()
    data class NavigateToRoom(val roomId: String) : UserCodeShareViewEvents()
    object CameraPermissionNotGranted : UserCodeShareViewEvents()
    data class SharePlainText(val text: String, val title: String, val richPlainText: String) : UserCodeShareViewEvents()
}

A vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +162 -0
@@ 0,0 1,162 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.usercode

import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.util.awaitCallback

class UserCodeSharedViewModel @AssistedInject constructor(
        @Assisted val initialState: UserCodeState,
        private val session: Session,
        private val stringProvider: StringProvider,
        private val rawService: RawService) : VectorViewModel<UserCodeState, UserCodeActions, UserCodeShareViewEvents>(initialState) {

    companion object : MvRxViewModelFactory<UserCodeSharedViewModel, UserCodeState> {
        override fun create(viewModelContext: ViewModelContext, state: UserCodeState): UserCodeSharedViewModel? {
            val factory = when (viewModelContext) {
                is FragmentViewModelContext -> viewModelContext.fragment as? Factory
                is ActivityViewModelContext -> viewModelContext.activity as? Factory
            }
            return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
        }
    }

    init {
        val user = session.getUser(initialState.userId)
        setState {
            copy(
                    matrixItem = user?.toMatrixItem(),
                    shareLink = session.permalinkService().createPermalink(initialState.userId)
            )
        }
    }

    @AssistedInject.Factory
    interface Factory {
        fun create(initialState: UserCodeState): UserCodeSharedViewModel
    }

    override fun handle(action: UserCodeActions) {
        when (action) {
            UserCodeActions.DismissAction              -> _viewEvents.post(UserCodeShareViewEvents.Dismiss)
            is UserCodeActions.SwitchMode              -> setState { copy(mode = action.mode) }
            is UserCodeActions.DecodedQRCode           -> handleQrCodeDecoded(action)
            is UserCodeActions.StartChattingWithUser   -> handleStartChatting(action)
            UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted)
            UserCodeActions.ShareByText                -> handleShareByText()
        }
    }

    private fun handleShareByText() {
        session.permalinkService().createPermalink(session.myUserId)?.let { permalink ->
            val text = stringProvider.getString(R.string.invite_friends_text, permalink)
            _viewEvents.post(UserCodeShareViewEvents.SharePlainText(
                    text,
                    stringProvider.getString(R.string.invite_friends),
                    stringProvider.getString(R.string.invite_friends_rich_title)
            ))
        }
    }

    private fun handleStartChatting(withUser: UserCodeActions.StartChattingWithUser) {
        val mxId = withUser.matrixItem.id
        val existing = session.getExistingDirectRoomWithUser(mxId)
        setState {
            copy(mode = UserCodeState.Mode.SHOW)
        }
        if (existing != null) {
            // navigate to this room
            _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(existing))
        } else {
            // we should create the room then navigate
            _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
            viewModelScope.launch(Dispatchers.IO) {
                val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
                        ?.isE2EByDefault()
                        ?: true

                val roomParams = CreateRoomParams()
                        .apply {
                            invitedUserIds.add(mxId)
                            setDirectMessage()
                            enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
                        }

                val roomId =
                        try {
                            awaitCallback<String> { session.createRoom(roomParams, it) }
                        } catch (failure: Throwable) {
                            _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure)))
                            return@launch
                        } finally {
                            _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
                        }
                _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId))
            }
        }
    }

    private fun handleQrCodeDecoded(action: UserCodeActions.DecodedQRCode) {
        val linkedId = PermalinkParser.parse(action.code)
        if (linkedId is PermalinkData.FallbackLink) {
            _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_a_valid_qr_code)))
            return
        }
        _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
        viewModelScope.launch(Dispatchers.IO) {
            when (linkedId) {
                is PermalinkData.RoomLink     -> TODO()
                is PermalinkData.UserLink     -> {
                    val user = session.getUser(linkedId.userId) ?: awaitCallback<List<User>> {
                        session.searchUsersDirectory(linkedId.userId, 10, emptySet(), it)
                    }.firstOrNull { it.userId == linkedId.userId }
                    // Create raw Uxid in case the user is not searchable
                    ?: User(linkedId.userId, null, null)

                    setState {
                        copy(
                                mode = UserCodeState.Mode.RESULT(user.toMatrixItem())
                        )
                    }
                }
                is PermalinkData.GroupLink    -> TODO()
                is PermalinkData.FallbackLink -> TODO()
            }
            _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
        }
    }
}

A vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt +37 -0
@@ 0,0 1,37 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.usercode

import com.airbnb.mvrx.MvRxState
import org.matrix.android.sdk.api.util.MatrixItem

data class UserCodeState(
        val userId: String,
        val matrixItem: MatrixItem? = null,
        val shareLink: String? = null,
        val mode: Mode = Mode.SHOW
) : MvRxState {
    sealed class Mode {
        object SHOW : Mode()
        object SCAN : Mode()
        data class RESULT(val matrixItem: MatrixItem) : Mode()
    }

    constructor(args: UserCodeActivity.Args) : this(
            userId = args.userId
    )
}

A vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt +54 -0
@@ 0,0 1,54 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.userdirectory

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.utils.DebouncedClickListener

@EpoxyModelClass(layout = R.layout.item_contact_action)
abstract class ActionItem : VectorEpoxyModel<ActionItem.Holder>() {

    @EpoxyAttribute var title: CharSequence? = null
    @EpoxyAttribute @DrawableRes var actionIconRes: Int? = null
    @EpoxyAttribute var clickAction: View.OnClickListener? = null

    override fun bind(holder: Holder) {
        super.bind(holder)
        holder.view.setOnClickListener(clickAction?.let { DebouncedClickListener(it) })
        // If name is empty, use userId as name and force it being centered
        holder.actionTitleText.setTextOrHide(title)
        if (actionIconRes != null) {
            holder.actionTitleImageView.setImageResource(actionIconRes!!)
        } else {
            holder.actionTitleImageView.setImageDrawable(null)
        }
    }

    class Holder : VectorEpoxyHolder() {
        val actionTitleText by bind<TextView>(R.id.actionTitleText)
        val actionTitleImageView by bind<ImageView>(R.id.actionIconImageView)
    }
}

A vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt +47 -0
@@ 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.app.features.userdirectory

import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide

@EpoxyModelClass(layout = R.layout.item_contact_detail)
abstract class ContactDetailItem : VectorEpoxyModel<ContactDetailItem.Holder>() {

    @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<TextView>(R.id.contactDetailName)
        val matrixIdView by bind<TextView>(R.id.contactDetailMatrixId)
    }
}

A vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt +46 -0
@@ 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.app.features.userdirectory

import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.contacts.MappedContact
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.AvatarRenderer

@EpoxyModelClass(layout = R.layout.item_contact_main)
abstract class ContactItem : VectorEpoxyModel<ContactItem.Holder>() {

    @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<TextView>(R.id.contactDisplayName)
        val avatarImageView by bind<ImageView>(R.id.contactAvatar)
    }
}

D vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt => vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt +0 -122
@@ 1,122 0,0 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.userdirectory

import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.EmptyItem_
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject

class KnownUsersController @Inject constructor(private val session: Session,
                                               private val avatarRenderer: AvatarRenderer,
                                               private val stringProvider: StringProvider) : PagedListEpoxyController<User>(
        modelBuildingHandler = createUIHandler()
) {

    private var selectedUsers: List<String> = emptyList()
    private var users: Async<List<User>> = Uninitialized
    private var isFiltering: Boolean = false

    var callback: Callback? = null

    init {
        requestModelBuild()
    }

    fun setData(state: UserDirectoryViewState) {
        this.isFiltering = !state.filterKnownUsersValue.isEmpty()
        val newSelection = state.getSelectedMatrixId()
        this.users = state.knownUsers
        if (newSelection != selectedUsers) {
            this.selectedUsers = newSelection
            requestForcedModelBuild()
        }
        submitList(state.knownUsers())
    }

    override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> {
        return if (item == null) {
            EmptyItem_().id(currentPosition)
        } else {
            val isSelected = selectedUsers.contains(item.userId)
            UserDirectoryUserItem_()
                    .id(item.userId)
                    .selected(isSelected)
                    .matrixItem(item.toMatrixItem())
                    .avatarRenderer(avatarRenderer)
                    .clickListener { _ ->
                        callback?.onItemClick(item)
                    }
        }
    }

    override fun addModels(models: List<EpoxyModel<*>>) {
        if (users is Incomplete) {
            renderLoading()
        } else if (models.isEmpty()) {
            renderEmptyState()
        } else {
            var lastFirstLetter: String? = null
            for (model in models) {
                if (model is UserDirectoryUserItem) {
                    if (model.matrixItem.id == session.myUserId) continue
                    val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName()
                    val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
                    lastFirstLetter = currentFirstLetter

                    UserDirectoryLetterHeaderItem_()
                            .id(currentFirstLetter)
                            .letter(currentFirstLetter)
                            .addIf(showLetter, this)

                    model.addTo(this)
                } else {
                    continue
                }
            }
        }
    }

    private fun renderLoading() {
        loadingItem {
            id("loading")
        }
    }

    private fun renderEmptyState() {
        noResultItem {
            id("noResult")
            text(stringProvider.getString(R.string.direct_room_no_known_users))
        }
    }

    interface Callback {
        fun onItemClick(user: User)
    }
}

D vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt +0 -94
@@ 1,94 0,0 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.userdirectory

import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setupAsSearch
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_user_directory.*
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject

class UserDirectoryFragment @Inject constructor(
        private val directRoomController: DirectoryUsersController
) : VectorBaseFragment(), DirectoryUsersController.Callback {

    override fun getLayoutResId() = R.layout.fragment_user_directory
    private val viewModel: UserDirectoryViewModel 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()
        setupSearchByMatrixIdView()
        setupCloseView()
    }

    override fun onDestroyView() {
        userDirectoryRecyclerView.cleanup()
        directRoomController.callback = null
        super.onDestroyView()
    }

    private fun setupRecyclerView() {
        directRoomController.callback = this
        userDirectoryRecyclerView.configureWith(directRoomController)
    }

    private fun setupSearchByMatrixIdView() {
        userDirectorySearchById.setupAsSearch(searchIconRes = 0)
        userDirectorySearchById
                .textChanges()
                .subscribe {
                    viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString()))
                }
                .disposeOnDestroyView()
        userDirectorySearchById.showKeyboard(andRequestFocus = true)
    }

    private fun setupCloseView() {
        userDirectoryClose.debouncedClicks {
            sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
        }
    }

    override fun invalidate() = withState(viewModel) {
        directRoomController.setData(it)
    }

    override fun onItemClick(user: User) {
        view?.hideKeyboard()
        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
        sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
    }

    override fun retryDirectoryUsersRequest() {
        val currentSearch = userDirectorySearchById.text.toString()
        viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch))
    }
}

R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryAction.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt +6 -6
@@ 18,10 18,10 @@ package im.vector.app.features.userdirectory

import im.vector.app.core.platform.VectorViewModelAction

sealed class UserDirectoryAction : VectorViewModelAction {
    data class FilterKnownUsers(val value: String) : UserDirectoryAction()
    data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
    object ClearFilterKnownUsers : UserDirectoryAction()
    data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
    data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
sealed class UserListAction : VectorViewModelAction {
    data class SearchUsers(val value: String) : UserListAction()
    object ClearSearchUsers : UserListAction()
    data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
    data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
    object ComputeMatrixToLinkForSharing : UserListAction()
}

R vector/src/main/java/im/vector/app/features/userdirectory/DirectoryUsersController.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt +124 -66
@@ 16,6 16,7 @@

package im.vector.app.features.userdirectory

import android.view.View
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading


@@ 28,112 29,169 @@ import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject

class DirectoryUsersController @Inject constructor(private val session: Session,
                                                   private val avatarRenderer: AvatarRenderer,
                                                   private val stringProvider: StringProvider,
                                                   private val errorFormatter: ErrorFormatter) : EpoxyController() {
class UserListController @Inject constructor(private val session: Session,
                                             private val avatarRenderer: AvatarRenderer,
                                             private val stringProvider: StringProvider,
                                             private val errorFormatter: ErrorFormatter) : EpoxyController() {

    private var state: UserDirectoryViewState? = null
    private var state: UserListViewState? = null

    var callback: Callback? = null

    init {
        requestModelBuild()
    }

    fun setData(state: UserDirectoryViewState) {
    fun setData(state: UserListViewState) {
        this.state = state
        requestModelBuild()
    }

    override fun buildModels() {
        val currentState = state ?: return
        val hasSearch = currentState.directorySearchTerm.isNotBlank()

        // Build generic items
        if (currentState.searchTerm.isBlank()) {
            // For now we remove this option if in invite to existing room flow (and not create DM)
            if (currentState.pendingInvitees.isEmpty()
                    // For now we remove this option if in invite to existing room flow (and not create DM)
                    && currentState.existingRoomId == null) {
                actionItem {
                    id(R.drawable.ic_share)
                    title(stringProvider.getString(R.string.invite_friends))
                    actionIconRes(R.drawable.ic_share)
                    clickAction(View.OnClickListener {
                        callback?.onInviteFriendClick()
                    })
                }
            }
            actionItem {
                id(R.drawable.ic_baseline_perm_contact_calendar_24)
                title(stringProvider.getString(R.string.contacts_book_title))
                actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24)
                clickAction(View.OnClickListener {
                    callback?.onContactBookClick()
                })
            }
            if (currentState.pendingInvitees.isEmpty()
                    // For now we remove this option if in invite to existing room flow (and not create DM)
                    && currentState.existingRoomId == null) {
                actionItem {
                    id(R.drawable.ic_qr_code_add)
                    title(stringProvider.getString(R.string.qr_code))
                    actionIconRes(R.drawable.ic_qr_code_add)
                    clickAction(View.OnClickListener {
                        callback?.onUseQRCode()
                    })
                }
            }
        }

        when (currentState.knownUsers) {
            is Uninitialized -> renderEmptyState()
            is Loading       -> renderLoading()
            is Fail          -> renderFailure(currentState.knownUsers.error)
            is Success       -> buildKnownUsers(currentState, currentState.getSelectedMatrixId())
        }

        when (val asyncUsers = currentState.directoryUsers) {
            is Uninitialized -> renderEmptyState(false)
            is Uninitialized -> {
            }
            is Loading       -> renderLoading()
            is Success       -> renderSuccess(
                    computeUsersList(asyncUsers(), currentState.directorySearchTerm),
            is Fail          -> renderFailure(asyncUsers.error)
            is Success       -> buildDirectoryUsers(
                    asyncUsers(),
                    currentState.getSelectedMatrixId(),
                    hasSearch
                    currentState.searchTerm,
                    // to avoid showing twice same user in known and suggestions
                    currentState.knownUsers.invoke()?.map { it.userId }.orEmpty()
            )
            is Fail          -> renderFailure(asyncUsers.error)
        }
    }

    /**
     * Eventually add the searched terms, if it is a userId, and if not already present in the result
     */
    private fun computeUsersList(directoryUsers: List<User>, searchTerms: String): List<User> {
        return directoryUsers +
                searchTerms
                        .takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } }
                        ?.let { listOf(User(it)) }
                        .orEmpty()
    }
    private fun buildKnownUsers(currentState: UserListViewState, selectedUsers: List<String>) {
        currentState.knownUsers()?.let { userList ->
            userListHeaderItem {
                id("known_header")
                header(stringProvider.getString(R.string.direct_room_user_list_known_title))
            }

    private fun renderLoading() {
        loadingItem {
            id("loading")
            if (userList.isEmpty()) {
                renderEmptyState()
                return
            }
            userList.forEach { item ->
                val isSelected = selectedUsers.contains(item.userId)
                userDirectoryUserItem {
                    id(item.userId)
                    selected(isSelected)
                    matrixItem(item.toMatrixItem())
                    avatarRenderer(avatarRenderer)
                    clickListener { _ ->
                        callback?.onItemClick(item)
                    }
                }
            }
        }
    }

    private fun renderFailure(failure: Throwable) {
        errorWithRetryItem {
            id("error")
            text(errorFormatter.toHumanReadable(failure))
            listener { callback?.retryDirectoryUsersRequest() }
    private fun buildDirectoryUsers(directoryUsers: List<User>, selectedUsers: List<String>, searchTerms: String, ignoreIds: List<String>) {
        val toDisplay = directoryUsers.filter { !ignoreIds.contains(it.userId) }
        if (toDisplay.isEmpty() && searchTerms.isBlank()) {
            return
        }
    }

    private fun renderSuccess(users: List<User>,
                              selectedUsers: List<String>,
                              hasSearch: Boolean) {
        if (users.isEmpty()) {
            renderEmptyState(hasSearch)
        } else {
            renderUsers(users, selectedUsers)
        userListHeaderItem {
            id("suggestions")
            header(stringProvider.getString(R.string.direct_room_user_list_suggestions_title))
        }
    }

    private fun renderUsers(users: List<User>, selectedUsers: List<String>) {
        for (user in users) {
            if (user.userId == session.myUserId) {
                continue
            }
            val isSelected = selectedUsers.contains(user.userId)
            userDirectoryUserItem {
                id(user.userId)
                selected(isSelected)
                matrixItem(user.toMatrixItem())
                avatarRenderer(avatarRenderer)
                clickListener { _ ->
                    callback?.onItemClick(user)
        if (toDisplay.isEmpty()) {
            renderEmptyState()
        } else {
            toDisplay.forEach { user ->
                if (user.userId != session.myUserId) {
                    val isSelected = selectedUsers.contains(user.userId)
                    userDirectoryUserItem {
                        id(user.userId)
                        selected(isSelected)
                        matrixItem(user.toMatrixItem())
                        avatarRenderer(avatarRenderer)
                        clickListener { _ ->
                            callback?.onItemClick(user)
                        }
                    }
                }
            }
        }
    }

    private fun renderEmptyState(hasSearch: Boolean) {
        val noResultRes = if (hasSearch) {
            R.string.no_result_placeholder
        } else {
            R.string.direct_room_start_search
    private fun renderLoading() {
        loadingItem {
            id("loading")
        }
    }

    private fun renderEmptyState() {
        noResultItem {
            id("noResult")
            text(stringProvider.getString(noResultRes))
            text(stringProvider.getString(R.string.no_result_placeholder))
        }
    }

    private fun renderFailure(failure: Throwable) {
        errorWithRetryItem {
            id("error")
            text(errorFormatter.toHumanReadable(failure))
        }
    }

    interface Callback {
        fun onInviteFriendClick()
        fun onContactBookClick()
        fun onUseQRCode()
        fun onItemClick(user: User)
        fun retryDirectoryUsersRequest()
        fun onMatrixIdClick(matrixId: String)
        fun onThreePidClick(threePid: ThreePid)
    }
}

R vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragment.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +75 -58
@@ 36,53 36,64 @@ import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setupAsSearch
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import kotlinx.android.synthetic.main.fragment_known_users.*
import kotlinx.android.synthetic.main.fragment_user_list.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject

class KnownUsersFragment @Inject constructor(
        val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory,
        private val knownUsersController: KnownUsersController,
class UserListFragment @Inject constructor(
        private val userListController: UserListController,
        private val dimensionConverter: DimensionConverter,
        val homeServerCapabilitiesViewModelFactory: HomeServerCapabilitiesViewModel.Factory
) : VectorBaseFragment(), KnownUsersController.Callback {
) : VectorBaseFragment(), UserListController.Callback {

    private val args: KnownUsersFragmentArgs by args()
    private val args: UserListFragmentArgs by args()
    private val viewModel: UserListViewModel by activityViewModel()
    private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel()
    private lateinit var sharedActionViewModel: UserListSharedActionViewModel

    override fun getLayoutResId() = R.layout.fragment_known_users
    override fun getLayoutResId() = R.layout.fragment_user_list

    override fun getMenuRes() = args.menuResId

    private val viewModel: UserDirectoryViewModel by activityViewModel()
    private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel()

    private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
        sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
        userListTitle.text = args.title
        vectorBaseActivity.setSupportActionBar(userListToolbar)

        knownUsersTitle.text = args.title
        vectorBaseActivity.setSupportActionBar(knownUsersToolbar)
        setupRecyclerView()
        setupFilterView()
        setupAddByMatrixIdView()
        setupAddFromPhoneBookView()
        setupSearchView()
        setupCloseView()

        homeServerCapabilitiesViewModel.subscribe {
            knownUsersE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
            userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
        }

        viewModel.selectSubscribe(this, UserDirectoryViewState::pendingInvitees) {
        viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) {
            renderSelectedUsers(it)
        }

        viewModel.observeViewEvents {
            when (it) {
                is UserListViewEvents.OpenShareMatrixToLing -> {
                    val text = getString(R.string.invite_friends_text, it.link)
                    startSharePlainTextIntent(
                            fragment = this,
                            activityResultLauncher = null,
                            chooserTitle = getString(R.string.invite_friends),
                            text = text,
                            extraTitle = getString(R.string.invite_friends_rich_title)
                    )
                }
            }
        }
    }

    override fun onDestroyView() {
        knownUsersController.callback = null
        knownUsersRecyclerView.cleanup()
        userListRecyclerView.cleanup()
        super.onDestroyView()
    }



@@ 91,69 102,52 @@ class KnownUsersFragment @Inject constructor(
            val showMenuItem = it.pendingInvitees.isNotEmpty()
            menu.forEach { menuItem ->
                menuItem.isVisible = showMenuItem
                if (args.isCreatingRoom) {
                    menuItem.setTitle(if (it.existingDmRoomId != null) R.string.action_open else R.string.create_room_action_create)
                }
            }
        }
        super.onPrepareOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
        sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(
                item.itemId,
                it.pendingInvitees,
                it.existingDmRoomId
        ))
        sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees))
        return@withState true
    }

    private fun setupAddByMatrixIdView() {
        addByMatrixId.debouncedClicks {
            sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory)
        }
    }

    private fun setupAddFromPhoneBookView() {
        addFromPhoneBook.debouncedClicks {
            // TODO handle Permission first
            sharedActionViewModel.post(UserDirectorySharedAction.OpenPhoneBook)
        }
    }

    private fun setupRecyclerView() {
        knownUsersController.callback = this
        userListController.callback = this
        // Don't activate animation as we might have way to much item animation when filtering
        knownUsersRecyclerView.configureWith(knownUsersController, disableItemAnimation = true)
        userListRecyclerView.configureWith(userListController, disableItemAnimation = true)
    }

    private fun setupFilterView() {
        knownUsersFilter
    private fun setupSearchView() {
        withState(viewModel) {
            userListSearch.hint = getString(R.string.user_directory_search_hint)
        }
        userListSearch
                .textChanges()
                .startWith(knownUsersFilter.text)
                .startWith(userListSearch.text)
                .subscribe { text ->
                    val filterValue = text.trim()
                    val action = if (filterValue.isBlank()) {
                        UserDirectoryAction.ClearFilterKnownUsers
                    val searchValue = text.trim()
                    val action = if (searchValue.isBlank()) {
                        UserListAction.ClearSearchUsers
                    } else {
                        UserDirectoryAction.FilterKnownUsers(filterValue.toString())
                        UserListAction.SearchUsers(searchValue.toString())
                    }
                    viewModel.handle(action)
                }
                .disposeOnDestroyView()

        knownUsersFilter.setupAsSearch()
        knownUsersFilter.requestFocus()
        userListSearch.setupAsSearch()
        userListSearch.requestFocus()
    }

    private fun setupCloseView() {
        knownUsersClose.debouncedClicks {
        userListClose.debouncedClicks {
            requireActivity().finish()
        }
    }

    override fun invalidate() = withState(viewModel) {
        knownUsersController.setData(it)
        userListController.setData(it)
    }

    private fun renderSelectedUsers(invitees: Set<PendingInvitee>) {


@@ 183,12 177,35 @@ class KnownUsersFragment @Inject constructor(
        chip.isCloseIconVisible = true
        chipGroup.addView(chip)
        chip.setOnCloseIconClickListener {
            viewModel.handle(UserDirectoryAction.RemovePendingInvitee(pendingInvitee))
            viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee))
        }
    }

    override fun onInviteFriendClick() {
        viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing)
    }

    override fun onContactBookClick() {
        sharedActionViewModel.post(UserListSharedAction.OpenPhoneBook)
    }

    override fun onItemClick(user: User) {
        view?.hideKeyboard()
        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
        viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
    }

    override fun onMatrixIdClick(matrixId: String) {
        view?.hideKeyboard()
        viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
    }

    override fun onThreePidClick(threePid: ThreePid) {
        view?.hideKeyboard()
        viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
    }

    override fun onUseQRCode() {
        view?.hideKeyboard()
        sharedActionViewModel.post(UserListSharedAction.AddByQrCode)
    }
}

R vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragmentArgs.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt +2 -2
@@ 20,9 20,9 @@ import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class KnownUsersFragmentArgs(
data class UserListFragmentArgs(
        val title: String,
        val menuResId: Int,
        val excludedUserIds: Set<String>? = null,
        val isCreatingRoom: Boolean = false
        val existingRoomId: String? = null
) : Parcelable

A vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt +39 -0
@@ 0,0 1,39 @@
/*
 * Copyright (c) 2020 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.app.features.userdirectory

import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel

@EpoxyModelClass(layout = R.layout.item_user_list_header)
abstract class UserListHeaderItem : VectorEpoxyModel<UserListHeaderItem.Holder>() {

    @EpoxyAttribute var header: String = ""

    override fun bind(holder: Holder) {
        super.bind(holder)
        holder.headerTextView.text = header
    }

    class Holder : VectorEpoxyHolder() {
        val headerTextView by bind<TextView>(R.id.userListHeaderView)
    }
}

R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedAction.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt +6 -8
@@ 18,12 18,10 @@ package im.vector.app.features.userdirectory

import im.vector.app.core.platform.VectorSharedAction

sealed class UserDirectorySharedAction : VectorSharedAction {
    object OpenUsersDirectory : UserDirectorySharedAction()
    object OpenPhoneBook : UserDirectorySharedAction()
    object Close : UserDirectorySharedAction()
    object GoBack : UserDirectorySharedAction()
    data class OnMenuItemSelected(val itemId: Int,
                                  val invitees: Set<PendingInvitee>,
                                  val existingDmRoomId: String?) : UserDirectorySharedAction()
sealed class UserListSharedAction : VectorSharedAction {
    object Close : UserListSharedAction()
    object GoBack : UserListSharedAction()
    data class OnMenuItemSelected(val itemId: Int, val invitees: Set<PendingInvitee>) : UserListSharedAction()
    object OpenPhoneBook : UserListSharedAction()
    object AddByQrCode : UserListSharedAction()
}

R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedActionViewModel.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedActionViewModel.kt +1 -1
@@ 19,4 19,4 @@ package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorSharedActionViewModel
import javax.inject.Inject

class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserDirectorySharedAction>()
class UserListSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserListSharedAction>()

R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewEvents.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt +3 -1
@@ 21,4 21,6 @@ import im.vector.app.core.platform.VectorViewEvents
/**
 * Transient events for invite users to room screen
 */
sealed class UserDirectoryViewEvents : VectorViewEvents
sealed class UserListViewEvents : VectorViewEvents {
    data class OpenShareMatrixToLing(val link: String) : UserListViewEvents()
}

R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewModel.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +97 -70
@@ 16,8 16,6 @@

package im.vector.app.features.userdirectory

import androidx.fragment.app.FragmentActivity
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory


@@ 28,126 26,155 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.invite.InviteUsersToRoomActivity
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit

private typealias KnowUsersFilter = String
private typealias KnownUsersSearch = String
private typealias DirectoryUsersSearch = String

class UserDirectoryViewModel @AssistedInject constructor(@Assisted
                                                         initialState: UserDirectoryViewState,
                                                         private val session: Session)
    : VectorViewModel<UserDirectoryViewState, UserDirectoryAction, UserDirectoryViewEvents>(initialState) {
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
                                                    private val session: Session)
    : VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {

    private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
    private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()

    private var currentUserSearchDisposable: Disposable? = null

    @AssistedInject.Factory
    interface Factory {
        fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel
        fun create(initialState: UserListViewState): UserListViewModel
    }

    private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
    private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()

    companion object : MvRxViewModelFactory<UserDirectoryViewModel, UserDirectoryViewState> {
    companion object : MvRxViewModelFactory<UserListViewModel, UserListViewState> {

        override fun create(viewModelContext: ViewModelContext, state: UserDirectoryViewState): UserDirectoryViewModel? {
            return when (viewModelContext) {
                is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state)
                is ActivityViewModelContext -> {
                    when (viewModelContext.activity<FragmentActivity>()) {
                        is CreateDirectRoomActivity  -> vi