QR code invite flow support - invite friends

Author: Valere
This commit is contained in:
Benoit Marty 2020-11-19 13:11:41 +01:00 committed by Valere
parent e8d084b855
commit 1070c23608
65 changed files with 2311 additions and 1188 deletions

View file

@ -3,6 +3,8 @@ 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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.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
import kotlinx.android.synthetic.main.vector_preference_push_rule.view.*
@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)
}
}

View file

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

View file

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

View file

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

View file

@ -45,23 +45,23 @@ import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
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
@ -71,37 +71,36 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
injector.inject(this)
}
override fun create(initialState: UserListViewState, args: UserListFragmentArgs): UserListViewModel {
return userListViewModelFactory.create(initialState, args)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
if (intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
if (isFirstCreation()) { openAddByQrCode() }
} else {
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
}
.disposeOnDestroy()
if (isFirstCreation()) {
addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room,
isCreatingRoom = true
)
)
}
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.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,
UserListFragment::class.java,
UserListFragmentArgs(
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room
)
)
}
viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
renderCreateAndInviteState(it)
@ -129,22 +128,22 @@ 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 && intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
} else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
}
} else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
finish()
}
}
}
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
))
}
}
@ -198,12 +197,9 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
}
companion object {
private const val BY_QR_CODE = "BY_QR_CODE"
fun getIntent(context: Context, byQrCode: Boolean = false): Intent {
return Intent(context, CreateDirectRoomActivity::class.java).apply {
putExtra(BY_QR_CODE, byQrCode)
}
fun getIntent(context: Context): Intent {
return Intent(context, CreateDirectRoomActivity::class.java)
}
}
}

View file

@ -21,7 +21,11 @@ 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
@ -36,16 +40,32 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen
override fun getLayoutResId() = R.layout.fragment_qr_code_scanner
override fun onResume() {
super.onResume()
// Register ourselves as a handler for scan results.
scannerView.setResultHandler(null)
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()
}
@ -73,23 +93,17 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen
requireActivity().finish()
} else {
val existingDm = viewModel.session.getExistingDirectRoomWithUser(mxid)
if (existingDm === null) {
// 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)))
)
}
} else {
navigator.openRoom(requireContext(), existingDm, null, false)
// 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)
)
}
}
}

View file

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

View file

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

View file

@ -45,7 +45,6 @@ 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.DmsFabMenuView
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.notifications.NotificationDrawerManager
import kotlinx.android.parcel.Parcelize
@ -67,7 +66,7 @@ class RoomListFragment @Inject constructor(
val roomListViewModelFactory: RoomListViewModel.Factory,
private val notificationDrawerManager: NotificationDrawerManager,
private val sharedViewPool: RecyclerView.RecycledViewPool
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, DmsFabMenuView.Listener, NotifsFabMenuView.Listener {
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, NotifsFabMenuView.Listener {
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@ -111,7 +110,6 @@ class RoomListFragment @Inject constructor(
}.exhaustive
}
createDmFabMenu.listener = this
createChatFabMenu.listener = this
sharedActionViewModel
@ -130,7 +128,6 @@ class RoomListFragment @Inject constructor(
roomListView.cleanup()
roomController.listener = null
stateRestorer.clear()
createDmFabMenu.listener = null
createChatFabMenu.listener = null
super.onDestroyView()
}
@ -142,32 +139,33 @@ class RoomListFragment @Inject constructor(
private fun setupCreateRoomButton() {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.isVisible = true
RoomListDisplayMode.PEOPLE -> createDmFabMenu.isVisible = true
RoomListDisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
RoomListDisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
else -> Unit // No button in this mode
}
createChatRoomButton.debouncedClicks {
createDirectChat()
}
createGroupRoomButton.debouncedClicks {
openRoomDirectory()
}
// Hide FABs when list is scrolling
// Hide FAB when list is scrolling
roomListView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
createDmFabMenu.removeCallbacks(showFabRunnable)
createChatFabMenu.removeCallbacks(showFabRunnable)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
createDmFabMenu.postDelayed(showFabRunnable, 250)
createChatFabMenu.postDelayed(showFabRunnable, 250)
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.hide()
RoomListDisplayMode.PEOPLE -> createDmFabMenu.hide()
RoomListDisplayMode.PEOPLE -> createChatRoomButton.hide()
RoomListDisplayMode.ROOMS -> createGroupRoomButton.hide()
else -> Unit
}
@ -192,10 +190,6 @@ class RoomListFragment @Inject constructor(
navigator.openCreateDirectRoom(requireActivity())
}
override fun createDirectChatByQrCode() {
navigator.openCreateDirectRoom(requireContext(), true)
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
@ -214,7 +208,7 @@ class RoomListFragment @Inject constructor(
if (isAdded) {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.show()
RoomListDisplayMode.PEOPLE -> createDmFabMenu.show()
RoomListDisplayMode.PEOPLE -> createChatRoomButton.show()
RoomListDisplayMode.ROOMS -> createGroupRoomButton.show()
else -> Unit
}
@ -343,9 +337,6 @@ class RoomListFragment @Inject constructor(
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
if (createDmFabMenu.onBackPressed()) {
return true
}
if (createChatFabMenu.onBackPressed()) {
return true
}

View file

@ -1,102 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list.widget
import android.content.Context
import android.util.AttributeSet
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_dms_fab_menu_merge.view.*
class DmsFabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
var listener: Listener? = null
init {
inflate(context, R.layout.motion_dms_fab_menu_merge, this)
}
override fun onFinishInflate() {
super.onFinishInflate()
listOf(createDmByMxid, createDmByMxidLabel)
.forEach {
it.setOnClickListener {
closeFabMenu()
listener?.createDirectChat()
}
}
listOf(createDmByQrCode, createDmByQrCodeLabel)
.forEach {
it.setOnClickListener {
closeFabMenu()
listener?.createDirectChatByQrCode()
}
}
dmsCreateRoomTouchGuard.setOnClickListener {
closeFabMenu()
}
}
override fun transitionToEnd() {
super.transitionToEnd()
dmsCreateRoomButton.contentDescription = context.getString(R.string.a11y_create_menu_close)
}
override fun transitionToStart() {
super.transitionToStart()
dmsCreateRoomButton.contentDescription = context.getString(R.string.a11y_create_menu_open)
}
fun show() {
isVisible = true
dmsCreateRoomButton.show()
}
fun hide() {
dmsCreateRoomButton.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
isVisible = false
}
})
}
private fun closeFabMenu() {
transitionToStart()
}
fun onBackPressed(): Boolean {
if (currentState == R.id.constraint_set_fab_menu_open) {
closeFabMenu()
return true
}
return false
}
interface Listener {
fun createDirectChat()
fun createDirectChatByQrCode()
}
}

View file

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

View file

@ -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, args: UserListFragmentArgs): UserListViewModel {
return userListViewModelFactory.create(initialState, args)
}
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()
}
}

View file

@ -0,0 +1,67 @@
/*
* 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.fragment_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.fragment_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 {
const val ARGS = "MatrixToFragment.Args"
fun create(matrixItem: MatrixItem, listener: InteractionListener?): MatrixToBottomSheet {
return MatrixToBottomSheet(matrixItem).apply {
interactionListener = listener
}
}
}
}

View file

@ -203,8 +203,8 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent)
}
override fun openCreateDirectRoom(context: Context, byQrCode: Boolean) {
val intent = CreateDirectRoomActivity.getIntent(context, byQrCode)
override fun openCreateDirectRoom(context: Context) {
val intent = CreateDirectRoomActivity.getIntent(context)
context.startActivity(intent)
}

View file

@ -56,7 +56,7 @@ interface Navigator {
fun openCreateRoom(context: Context, initialName: String = "")
fun openCreateDirectRoom(context: Context, byQrCode: Boolean = false)
fun openCreateDirectRoom(context: Context)
fun openInviteUsersToRoom(context: Context, roomId: String)

View file

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

View file

@ -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()
}
}
}

View file

@ -0,0 +1,135 @@
/*
* 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.app.Activity
import android.os.Bundle
import android.view.View
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.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()
}
}
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 onResume() {
super.onResume()
// Register ourselves as a handler for scan results.
userCodeScannerView.setResultHandler(this)
// Start camera on resume
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
}
override fun onPause() {
super.onPause()
// 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
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.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()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showUserCodeClose.debouncedClicks {
sharedViewModel.handle(UserCodeActions.DismissAction)
}
showUserCodeScanButton.debouncedClicks {
doOpenQRCodeScanner()
}
}
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)
Unit
}
}

View file

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

View file

@ -0,0 +1,126 @@
/*
* 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.exhaustive
import im.vector.app.core.platform.VectorBaseActivity
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) {
is UserCodeShareViewEvents.InviteFriend -> TODO()
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)
}.exhaustive
}
}
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
supportFragmentManager.beginTransaction().let {
it.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
it.replace(R.id.simpleFragmentContainer,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
it.commit()
}
}
}
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))
}
}
override fun create(initialState: UserCodeState, args: Args) =
viewModelFactory.create(initialState, args)
companion object {
fun newIntent(context: Context, userId: String): Intent {
return Intent(context, UserCodeActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(userId))
}
}
}
}

View file

@ -0,0 +1,28 @@
/*
* 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 {
data class InviteFriend(val permalink: String) : UserCodeShareViewEvents()
object Dismiss : UserCodeShareViewEvents()
object ShowWaitingScreen : UserCodeShareViewEvents()
object HideWaitingScreen : UserCodeShareViewEvents()
data class ToastMessage(val message: String) : UserCodeShareViewEvents()
data class NavigateToRoom(val roomId: String) : UserCodeShareViewEvents()
}

View file

@ -0,0 +1,161 @@
/*
* 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,
@Assisted val args: UserCodeActivity.Args,
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 args = viewModelContext.args<UserCodeActivity.Args>()
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state, args) ?: error("You should let your activity/fragment implements Factory interface")
}
override fun initialState(viewModelContext: ViewModelContext): UserCodeState? {
return UserCodeState(viewModelContext.args<UserCodeActivity.Args>().userId)
}
}
init {
val user = session.getUser(args.userId)
setState {
copy(
matrixItem = user?.toMatrixItem(),
shareLink = session.permalinkService().createPermalink(args.userId)
)
}
}
private fun handleInviteFriend() {
session.permalinkService().createPermalink(initialState.userId)?.let { permalink ->
_viewEvents.post(UserCodeShareViewEvents.InviteFriend(permalink))
}
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserCodeState, args: UserCodeActivity.Args): 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)
}
}
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 -> {
var 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)
}
}
}

View file

@ -0,0 +1,33 @@
/*
* 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()
}
}

View file

@ -0,0 +1,53 @@
/*
* 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
@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)
// 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)
}
}

View file

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

View file

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

View file

@ -1,139 +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.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
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.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() {
private var state: UserDirectoryViewState? = null
var callback: Callback? = null
init {
requestModelBuild()
}
fun setData(state: UserDirectoryViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
val hasSearch = currentState.directorySearchTerm.isNotBlank()
when (val asyncUsers = currentState.directoryUsers) {
is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading()
is Success -> renderSuccess(
computeUsersList(asyncUsers(), currentState.directorySearchTerm),
currentState.getSelectedMatrixId(),
hasSearch
)
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 renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderFailure(failure: Throwable) {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(failure))
listener { callback?.retryDirectoryUsersRequest() }
}
}
private fun renderSuccess(users: List<User>,
selectedUsers: List<String>,
hasSearch: Boolean) {
if (users.isEmpty()) {
renderEmptyState(hasSearch)
} else {
renderUsers(users, selectedUsers)
}
}
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)
}
}
}
}
private fun renderEmptyState(hasSearch: Boolean) {
val noResultRes = if (hasSearch) {
R.string.no_result_placeholder
} else {
R.string.direct_room_start_search
}
noResultItem {
id("noResult")
text(stringProvider.getString(noResultRes))
}
}
interface Callback {
fun onItemClick(user: User)
fun retryDirectoryUsersRequest()
}
}

View file

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

View file

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

View file

@ -1,153 +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 androidx.fragment.app.FragmentActivity
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
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 org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String
class UserDirectoryViewModel @AssistedInject constructor(@Assisted
initialState: UserDirectoryViewState,
private val session: Session)
: VectorViewModel<UserDirectoryViewState, UserDirectoryAction, UserDirectoryViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel
}
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
companion object : MvRxViewModelFactory<UserDirectoryViewModel, UserDirectoryViewState> {
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 -> viewModelContext.activity<CreateDirectRoomActivity>().userDirectoryViewModelFactory.create(state)
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().userDirectoryViewModelFactory.create(state)
else -> error("Wrong activity or fragment")
}
}
else -> error("Wrong activity or fragment")
}
}
}
init {
observeKnownUsers()
observeDirectoryUsers()
}
override fun handle(action: UserDirectoryAction) {
when (action) {
is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
is UserDirectoryAction.SelectPendingInvitee -> handleSelectUser(action)
is UserDirectoryAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
}.exhaustive
}
private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemovePendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
setState {
copy(
pendingInvitees = selectedUsers,
existingDmRoomId = getExistingDmRoomId(selectedUsers)
)
}
}
private fun handleSelectUser(action: UserDirectoryAction.SelectPendingInvitee) = withState { state ->
// Reset the filter asap
directoryUsersSearch.accept("")
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
setState {
copy(
pendingInvitees = selectedUsers,
existingDmRoomId = getExistingDmRoomId(selectedUsers)
)
}
}
private fun getExistingDmRoomId(selectedUsers: Set<PendingInvitee>): String? {
return selectedUsers
.takeIf { it.size == 1 }
?.filterIsInstance(PendingInvitee.UserPendingInvitee::class.java)
?.firstOrNull()
?.let { invitee -> session.getExistingDirectRoomWithUser(invitee.user.userId) }
}
private fun observeDirectoryUsers() = withState { state ->
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx()
.searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, directorySearchTerm = search)
}
}
.subscribe()
.disposeOnClear()
}
private fun observeKnownUsers() = withState { state ->
knownUsersFilter
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it.orNull(), state.excludedUserIds)
}
.execute { async ->
copy(
knownUsers = async,
filterKnownUsersValue = knownUsersFilter.value ?: Option.empty()
)
}
}
}

View file

@ -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()
}

View file

@ -0,0 +1,197 @@
/*
* 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 com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
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.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 UserListController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() {
private var state: UserListViewState? = null
var callback: Callback? = null
fun setData(state: UserListViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
// 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_invite_people)
title(stringProvider.getString(R.string.invite_friends))
actionIconRes(R.drawable.ic_invite_people)
clickAction(View.OnClickListener {
callback?.onInviteFriendClick()
})
}
}
actionItem {
id(R.drawable.ic_book)
title(stringProvider.getString(R.string.contacts_book_title))
actionIconRes(R.drawable.ic_book)
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 -> {
}
is Loading -> renderLoading()
is Fail -> renderFailure(asyncUsers.error)
is Success -> buildDirectoryUsers(
asyncUsers(),
currentState.getSelectedMatrixId(),
currentState.searchTerm,
// to avoid showing twice same user in known and suggestions
currentState.knownUsers.invoke()?.map { it.userId } ?: emptyList()
)
}
}
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))
}
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 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
}
userListHeaderItem {
id("suggestions")
header(stringProvider.getString(R.string.direct_room_user_list_suggestions_title))
}
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 renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderEmptyState() {
noResultItem {
id("noResult")
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 onMatrixIdClick(matrixId: String)
fun onThreePidClick(threePid: ThreePid)
}
}

View file

@ -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()
recyclerView.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)
recyclerView.configureWith(userListController, disableItemAnimation = true)
}
private fun setupFilterView() {
knownUsersFilter
private fun setupSearchView() {
withState(viewModel) {
userListSearch.hint = getString(R.string.user_directory_search_hint, it.myUserId)
}
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)
}
}

View file

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

View file

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

View file

@ -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()
}

View file

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

View file

@ -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()
}

View file

@ -0,0 +1,204 @@
/*
* 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.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
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 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.toMatrixItem
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
private typealias KnownUsersSearch = String
private typealias DirectoryUsersSearch = String
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
@Assisted args: UserListFragmentArgs,
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: UserListViewState, args: UserListFragmentArgs): UserListViewModel
}
companion object : MvRxViewModelFactory<UserListViewModel, UserListViewState> {
private val USER_NOT_FOUND_MAP = emptyMap<String, Any>()
private val USER_NOT_FOUND = User("")
override fun create(viewModelContext: ViewModelContext, state: UserListViewState): UserListViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
val args = viewModelContext.args<UserListFragmentArgs>()
return factory?.create(state, args) ?: error("You should let your activity/fragment implements Factory interface")
}
}
init {
setState {
copy(
myUserId = session.myUserId,
existingRoomId = args.existingRoomId
)
}
observeUsers()
}
override fun handle(action: UserListAction) {
when (action) {
is UserListAction.SearchUsers -> handleSearchUsers(action.value)
is UserListAction.ClearSearchUsers -> handleClearSearchUsers()
is UserListAction.SelectPendingInvitee -> handleSelectUser(action)
is UserListAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
}.exhaustive
}
private fun handleSearchUsers(searchTerm: String) {
setState {
copy(searchTerm = searchTerm)
}
knownUsersSearch.accept(searchTerm)
directoryUsersSearch.accept(searchTerm)
}
private fun handleShareMyMatrixToLink() {
session.permalinkService().createPermalink(session.myUserId)?.let {
_viewEvents.post(UserListViewEvents.OpenShareMatrixToLing(it))
}
}
private fun handleClearSearchUsers() {
knownUsersSearch.accept("")
directoryUsersSearch.accept("")
setState {
copy(searchTerm = "")
}
}
private fun observeUsers() = withState { state ->
knownUsersSearch
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it, state.excludedUserIds)
}
.execute { async ->
copy(knownUsers = async)
}
currentUserSearchDisposable?.dispose()
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else if (MatrixPatterns.isUserId(search)) {
// If it's a valid user id try to use Profile API
// because directory only returns users that are in public rooms or share a room with you, where as
// profile will work other federations
session.rx().searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
.zipWith(
session.rx().getProfileInfo(search)
// ... not sure how to handle that properly (manage error case in map and return optional)
.onErrorReturn { USER_NOT_FOUND_MAP }
.map { json ->
if (json === USER_NOT_FOUND_MAP) {
USER_NOT_FOUND
} else {
User(
userId = search,
displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
)
}
},
{ t1, t2 ->
if (t2 == USER_NOT_FOUND) {
t1
}
// profile result might also be in search results, in this case keep search result
else if (t1.indexOfFirst { it.userId == t2.userId } != -1) {
t1
} else {
// put it first
listOf(t2) + t1
}
}
)
.doOnSubscribe {
currentUserSearchDisposable = it
}
.doOnDispose {
currentUserSearchDisposable = null
}
} else {
session.rx()
.searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
.doOnSubscribe {
currentUserSearchDisposable = it
}
.doOnDispose {
currentUserSearchDisposable = null
}
}
stream.toAsync {
copy(directoryUsers = it)
}
}
.subscribe()
.disposeOnClear()
}
private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
}
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
}
}

View file

@ -17,30 +17,29 @@
package im.vector.app.features.userdirectory
import androidx.paging.PagedList
import arrow.core.Option
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.core.contacts.MappedContact
import org.matrix.android.sdk.api.session.user.model.User
data class UserDirectoryViewState(
data class UserListViewState(
val excludedUserIds: Set<String>? = null,
val knownUsers: Async<PagedList<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized,
val filteredMappedContacts: List<MappedContact> = emptyList(),
val pendingInvitees: Set<PendingInvitee> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val directorySearchTerm: String = "",
val filterKnownUsersValue: Option<String> = Option.empty(),
val existingDmRoomId: String? = null
val searchTerm: String = "",
val myUserId: String = "",
val existingRoomId: String? = null
) : MvRxState {
constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds)
fun getSelectedMatrixId(): List<String> {
return pendingInvitees
.mapNotNull {
when (it) {
is PendingInvitee.UserPendingInvitee -> it.user.userId
is PendingInvitee.UserPendingInvitee -> it.user.userId
is PendingInvitee.ThreePidPendingInvitee -> null
}
}

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,19.5C4,18.1193 5.1193,17 6.5,17H20"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6.5,2H20V22H6.5C5.1193,22 4,20.8807 4,19.5V4.5C4,3.1193 5.1193,2 6.5,2Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19.1001,9C18.7779,9 18.5168,8.7388 18.5168,8.4167V6.0833H16.1834C15.8613,6.0833 15.6001,5.8222 15.6001,5.5C15.6001,5.1778 15.8613,4.9167 16.1834,4.9167H18.5168V2.5833C18.5168,2.2612 18.7779,2 19.1001,2C19.4223,2 19.6834,2.2612 19.6834,2.5833V4.9167H22.0168C22.3389,4.9167 22.6001,5.1778 22.6001,5.5C22.6001,5.8222 22.3389,6.0833 22.0168,6.0833H19.6834V8.4167C19.6834,8.7388 19.4223,9 19.1001,9ZM19.6001,11C20.0669,11 20.5212,10.9467 20.9574,10.8458C21.1161,11.5383 21.2,12.2594 21.2,13C21.2,16.1409 19.6917,18.9294 17.3598,20.6808V20.6807C16.0014,21.7011 14.3635,22.3695 12.5815,22.5505C12.2588,22.5832 11.9314,22.6 11.6,22.6C6.2981,22.6 2,18.302 2,13C2,7.6981 6.2981,3.4 11.6,3.4C12.3407,3.4 13.0618,3.4839 13.7543,3.6427C13.6534,4.0788 13.6001,4.5332 13.6001,5C13.6001,8.3137 16.2864,11 19.6001,11ZM11.5999,20.68C13.6754,20.68 15.5585,19.8567 16.9407,18.5189C16.0859,16.4086 14.0167,14.92 11.5998,14.92C9.183,14.92 7.1138,16.4086 6.259,18.5189C7.6411,19.8567 9.5244,20.68 11.5999,20.68ZM11.7426,7.4117C10.3168,7.5417 9.2,8.7404 9.2,10.2C9.2,11.7464 10.4536,13 12,13C13.0308,13 13.9315,12.443 14.4176,11.6135C13.0673,10.6058 12.0929,9.1225 11.7426,7.4117Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3,5C3,3.8954 3.8954,3 5,3H19C20.1046,3 21,3.8954 21,5V19C21,20.1046 20.1046,21 19,21H5C3.8954,21 3,20.1046 3,19V5Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M8.5,10C9.3284,10 10,9.3284 10,8.5C10,7.6716 9.3284,7 8.5,7C7.6716,7 7,7.6716 7,8.5C7,9.3284 7.6716,10 8.5,10Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M21,15L16,10L5,21"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,72 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:pathData="M29.0625,8.4375C28.5447,8.4375 28.125,8.0177 28.125,7.5L28.125,2.6953C28.125,2.243 27.757,1.875 27.3047,1.875L22.5,1.875C21.9822,1.875 21.5625,1.4552 21.5625,0.9375C21.5625,0.4198 21.9822,0 22.5,0L27.3047,0C28.7909,0 30,1.2091 30,2.6953L30,7.5C30,8.0177 29.5803,8.4375 29.0625,8.4375Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M0.9375,8.4375C0.4197,8.4375 0,8.0177 0,7.5L0,2.6953C0,1.2091 1.2091,0 2.6953,0L7.5,0C8.0178,0 8.4375,0.4198 8.4375,0.9375C8.4375,1.4552 8.0178,1.875 7.5,1.875L2.6953,1.875C2.243,1.875 1.875,2.243 1.875,2.6953L1.875,7.5C1.875,8.0177 1.4553,8.4375 0.9375,8.4375Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M7.5,30L2.6953,30C1.2091,30 0,28.7909 0,27.3047L0,22.5C0,21.9823 0.4197,21.5625 0.9375,21.5625C1.4553,21.5625 1.875,21.9823 1.875,22.5L1.875,27.3047C1.875,27.757 2.243,28.125 2.6953,28.125L7.5,28.125C8.0178,28.125 8.4375,28.5448 8.4375,29.0625C8.4375,29.5802 8.0178,30 7.5,30Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M27.3047,30L22.5,30C21.9822,30 21.5625,29.5802 21.5625,29.0625C21.5625,28.5448 21.9822,28.125 22.5,28.125L27.3047,28.125C27.757,28.125 28.125,27.757 28.125,27.3047L28.125,22.5C28.125,21.9823 28.5447,21.5625 29.0625,21.5625C29.5803,21.5625 30,21.9823 30,22.5L30,27.3047C30,28.7909 28.7909,30 27.3047,30Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M11.3672,14.0625L6.4453,14.0625C4.9591,14.0625 3.75,12.8534 3.75,11.3672L3.75,6.4453C3.75,4.9591 4.9591,3.75 6.4453,3.75L11.3672,3.75C12.8534,3.75 14.0625,4.9591 14.0625,6.4453L14.0625,11.3672C14.0625,12.8534 12.8534,14.0625 11.3672,14.0625ZM6.4453,5.625C5.993,5.625 5.625,5.993 5.625,6.4453L5.625,11.3672C5.625,11.8195 5.993,12.1875 6.4453,12.1875L11.3672,12.1875C11.8195,12.1875 12.1875,11.8195 12.1875,11.3672L12.1875,6.4453C12.1875,5.993 11.8195,5.625 11.3672,5.625L6.4453,5.625Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M11.3672,26.25L6.4453,26.25C4.9591,26.25 3.75,25.0409 3.75,23.5547L3.75,18.6328C3.75,17.1466 4.9591,15.9375 6.4453,15.9375L11.3672,15.9375C12.8534,15.9375 14.0625,17.1466 14.0625,18.6328L14.0625,23.5547C14.0625,25.0409 12.8534,26.25 11.3672,26.25ZM6.4453,17.8125C5.993,17.8125 5.625,18.1805 5.625,18.6328L5.625,23.5547C5.625,24.007 5.993,24.375 6.4453,24.375L11.3672,24.375C11.8195,24.375 12.1875,24.007 12.1875,23.5547L12.1875,18.6328C12.1875,18.1805 11.8195,17.8125 11.3672,17.8125L6.4453,17.8125Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M23.5547,14.0625L18.6328,14.0625C17.1466,14.0625 15.9375,12.8534 15.9375,11.3672L15.9375,6.4453C15.9375,4.9591 17.1466,3.75 18.6328,3.75L23.5547,3.75C25.0409,3.75 26.25,4.9591 26.25,6.4453L26.25,11.3672C26.25,12.8534 25.0409,14.0625 23.5547,14.0625ZM18.6328,5.625C18.1805,5.625 17.8125,5.993 17.8125,6.4453L17.8125,11.3672C17.8125,11.8195 18.1805,12.1875 18.6328,12.1875L23.5547,12.1875C24.007,12.1875 24.375,11.8195 24.375,11.3672L24.375,6.4453C24.375,5.993 24.007,5.625 23.5547,5.625L18.6328,5.625Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M21.918,21.918L21.918,25.2871C21.918,25.7563 21.5376,26.1367 21.0684,26.1367C20.5991,26.1367 20.2188,25.7563 20.2188,25.2871L20.2188,21.918L16.8496,21.918C16.3804,21.918 16,21.5376 16,21.0684C16,20.5991 16.3804,20.2188 16.8496,20.2188L20.2188,20.2188L20.2188,16.8496C20.2188,16.3804 20.5991,16 21.0684,16C21.5376,16 21.918,16.3804 21.918,16.8496L21.918,20.2188L25.2871,20.2188C25.7563,20.2188 26.1367,20.5991 26.1367,21.0684C26.1367,21.5376 25.7563,21.918 25.2871,21.918L21.918,21.918Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M9.375,10.3125L8.4375,10.3125C7.9197,10.3125 7.5,9.8927 7.5,9.375L7.5,8.4375C7.5,7.9198 7.9197,7.5 8.4375,7.5L9.375,7.5C9.8928,7.5 10.3125,7.9198 10.3125,8.4375L10.3125,9.375C10.3125,9.8927 9.8928,10.3125 9.375,10.3125Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M21.5625,10.3125L20.625,10.3125C20.1072,10.3125 19.6875,9.8927 19.6875,9.375L19.6875,8.4375C19.6875,7.9198 20.1072,7.5 20.625,7.5L21.5625,7.5C22.0803,7.5 22.5,7.9198 22.5,8.4375L22.5,9.375C22.5,9.8927 22.0803,10.3125 21.5625,10.3125Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M9.375,22.5L8.4375,22.5C7.9197,22.5 7.5,22.0802 7.5,21.5625L7.5,20.625C7.5,20.1073 7.9197,19.6875 8.4375,19.6875L9.375,19.6875C9.8928,19.6875 10.3125,20.1073 10.3125,20.625L10.3125,21.5625C10.3125,22.0802 9.8928,22.5 9.375,22.5Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/vector_coordinator_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -10,4 +10,24 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout
android:id="@+id/simpleActivityWaitingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/colorBackgroundFloating"
android:gravity="center"
android:padding="8dp"
android:visibility="gone"
tools:visibility="visible">
<ProgressBar
android:id="@+id/waiting_view_status_circular_progress"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerInParent="true"
android:layout_gravity="center_vertical" />
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -31,10 +31,11 @@
<ImageView
android:id="@+id/homeDrawerHeaderAvatarView"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="24dp"
android:transitionName="profile"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
@ -43,13 +44,13 @@
android:id="@+id/homeDrawerUsernameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/homeDrawerQRCodeButton"
app:layout_constraintStart_toStartOf="@+id/homeDrawerHeaderAvatarView"
app:layout_constraintTop_toBottomOf="@+id/homeDrawerHeaderAvatarView"
tools:text="@sample/matrix.json/data/displayName" />
@ -58,18 +59,69 @@
android:id="@+id/homeDrawerUserIdView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="17dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/homeDrawerInviteFriendButton"
app:layout_constraintEnd_toStartOf="@+id/homeDrawerQRCodeButton"
app:layout_constraintStart_toStartOf="@+id/homeDrawerHeaderAvatarView"
app:layout_constraintTop_toBottomOf="@+id/homeDrawerUsernameView"
tools:text="@sample/matrix.json/data/mxid" />
<com.google.android.material.button.MaterialButton
android:id="@+id/homeDrawerQRCodeButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:backgroundTint="?riotx_bottom_nav_background_color"
android:elevation="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:cornerRadius="17dp"
app:icon="@drawable/ic_qr_code_add"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="20dp"
app:iconTint="@color/riotx_accent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/homeDrawerUsernameView" />
<com.google.android.material.button.MaterialButton
android:id="@+id/homeDrawerInviteFriendButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:gravity="center"
android:insetTop="0dp"
android:insetBottom="0dp"
android:padding="0dp"
android:text="@string/invite_friends"
android:textAllCaps="false"
android:textColor="?colorAccent"
android:textSize="13sp"
app:icon="@drawable/ic_invite_people"
app:iconGravity="textStart"
app:iconSize="20dp"
app:iconTint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/homeDrawerUserIdView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.fragment.app.FragmentContainerView

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/matrixToCardAvatar"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:elevation="4dp"
android:transitionName="profile"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/matrixToCardNameText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:layout_marginEnd="16dp"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="center"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@+id/matrixToCardAvatar"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/matrixToCardUserIdText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="center"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/matrixToCardNameText"
tools:text="@sample/matrix.json/data/mxid" />
<com.google.android.material.button.MaterialButton
android:id="@+id/matrixToCardSendMessageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:minWidth="130dp"
android:text="@string/start_chatting"
app:icon="@drawable/ic_fab_add_chat"
app:iconTint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/matrixToCardUserIdText"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -15,4 +15,28 @@
<!-- TODO In the future we could add a toggle to switch the flash, and other possible settings -->
<!-- TODO add take from album option.. -->
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/openAlbumButton"-->
<!-- style="@style/Widget.MaterialComponents.Button.Icon"-->
<!-- android:layout_width="34dp"-->
<!-- android:layout_height="34dp"-->
<!-- android:layout_marginEnd="@dimen/layout_horizontal_margin"-->
<!-- android:layout_marginBottom="@dimen/layout_vertical_margin_big"-->
<!-- android:backgroundTint="?riotx_bottom_nav_background_color"-->
<!-- android:elevation="0dp"-->
<!-- android:insetLeft="0dp"-->
<!-- android:insetTop="0dp"-->
<!-- android:insetRight="0dp"-->
<!-- android:insetBottom="0dp"-->
<!-- android:padding="0dp"-->
<!-- app:cornerRadius="17dp"-->
<!-- app:icon="@drawable/ic_picture_icon"-->
<!-- app:iconGravity="textStart"-->
<!-- app:iconPadding="0dp"-->
<!-- app:iconSize="20dp"-->
<!-- app:iconTint="@color/riotx_accent"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toEndOf="parent"/>-->
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/userCodeScannerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/userCodeMyCodeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:maxWidth="160dp"
android:text="@string/user_code_my_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/userCodeOpenGalleryButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:backgroundTint="?riotx_bottom_nav_background_color"
android:elevation="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:cornerRadius="17dp"
app:icon="@drawable/ic_picture_icon"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="20dp"
app:iconTint="@color/riotx_accent"
app:layout_constraintBottom_toBottomOf="@id/userCodeMyCodeButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/userCodeMyCodeButton"
app:layout_constraintTop_toTopOf="@id/userCodeMyCodeButton" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -23,16 +23,20 @@
tools:showPaths="true"
tools:visibility="visible" />
<im.vector.app.features.home.room.list.widget.DmsFabMenuView
android:id="@+id/createDmFabMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/createChatRoomButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:accessibilityTraversalBefore="@+id/roomListView"
android:contentDescription="@string/a11y_create_direct_message"
android:scaleType="center"
android:src="@drawable/ic_fab_add_chat"
android:visibility="gone"
app:maxImageSize="34dp"
app:layoutDescription="@xml/motion_scene_dms_fab_menu"
tools:showPaths="true"
tools:layout_marginEnd="80dp"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton

View file

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/showUserCodeToolBar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:elevation="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/showUserCodeClose"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:scaleType="center"
android:src="@drawable/ic_x_18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/showUserCodeTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/add_by_qr_code"
android:textColor="?riotx_text_primary"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/showUserCodeClose"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<ImageView
android:id="@+id/showUserCodeAvatar"
android:transitionName="profile"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:elevation="4dp"
app:layout_constraintBottom_toBottomOf="@id/showUserCodeCardTopBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/showUserCodeCardTopBarrier"
tools:src="@tools:sample/avatars" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/showUserCodeCardTopBarrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="showUserCodeCard" />
<androidx.cardview.widget.CardView
android:id="@+id/showUserCodeCard"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="50dp"
android:padding="16dp"
app:cardCornerRadius="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/showUserCodeToolBar"
app:layout_constraintWidth_percent="0.8">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="300dp">
<TextView
android:id="@+id/showUserCodeCardNameText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="16dp"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="center"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/showUserCodeCardUserIdText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="center"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/showUserCodeCardNameText"
tools:text="@sample/matrix.json/data/mxid" />
<!-- android:id="@+id/itemShareQrCodeImage"-->
<!-- android:layout_width="300dp"-->
<!-- android:layout_height="300dp"-->
<!-- android:layout_gravity="center_horizontal"-->
<!-- android:contentDescription="@string/a11y_qr_code_for_verification"-->
<!-- tools:src="@color/riotx_header_panel_background_black" />-->
<im.vector.app.core.ui.views.QrCodeImageView
android:id="@+id/showUserCodeQRImage"
android:layout_width="260dp"
android:layout_height="260dp"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:layout_marginBottom="@dimen/layout_vertical_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/showUserCodeCardUserIdText"
tools:src="@drawable/ic_qr_code_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/showUserCodeInfoText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/user_code_info_text"
android:textAlignment="center"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="@id/showUserCodeCard"
app:layout_constraintStart_toStartOf="@id/showUserCodeCard"
app:layout_constraintTop_toBottomOf="@id/showUserCodeCard" />
<com.google.android.material.button.MaterialButton
android:id="@+id/showUserCodeScanButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minWidth="130dp"
android:text="@string/user_code_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/showUserCodeInfoText"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -10,7 +10,7 @@
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/knownUsersToolbar"
android:id="@+id/userListToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
@ -24,7 +24,7 @@
android:layout_height="match_parent">
<ImageView
android:id="@+id/knownUsersClose"
android:id="@+id/userListClose"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:clickable="true"
@ -37,7 +37,7 @@
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/knownUsersTitle"
android:id="@+id/userListTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
@ -51,7 +51,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/knownUsersClose"
app:layout_constraintStart_toEndOf="@+id/userListClose"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -67,7 +67,7 @@
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/knownUsersToolbar"
app:layout_constraintTop_toBottomOf="@+id/userListToolbar"
app:maxHeight="64dp">
<com.google.android.material.chip.ChipGroup
@ -79,7 +79,7 @@
</im.vector.app.core.platform.MaxHeightScrollView>
<EditText
android:id="@+id/knownUsersFilter"
android:id="@+id/userListSearch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
@ -87,7 +87,7 @@
android:background="@null"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:hint="@string/direct_room_filter_hint"
android:hint="@string/user_directory_search_hint"
android:importantForAutofill="no"
android:inputType="text"
android:maxHeight="80dp"
@ -98,17 +98,17 @@
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollView" />
<View
android:id="@+id/knownUsersFilterDivider"
android:id="@+id/userListFilterDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/knownUsersFilter" />
app:layout_constraintTop_toBottomOf="@+id/userListSearch" />
<TextView
android:id="@+id/knownUsersE2EbyDefaultDisabled"
android:id="@+id/userListE2EbyDefaultDisabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
@ -117,44 +117,9 @@
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/knownUsersFilterDivider"
app:layout_constraintTop_toBottomOf="@id/userListFilterDivider"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/addByMatrixId"
style="@style/VectorButtonStyleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:minHeight="@dimen/layout_touch_size"
android:text="@string/add_by_matrix_id"
android:visibility="visible"
app:icon="@drawable/ic_plus_circle"
app:iconPadding="13dp"
app:iconTint="@color/riotx_accent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/knownUsersE2EbyDefaultDisabled" />
<com.google.android.material.button.MaterialButton
android:id="@+id/addFromPhoneBook"
style="@style/VectorButtonStyleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:minHeight="@dimen/layout_touch_size"
android:text="@string/search_in_my_contacts"
android:visibility="visible"
app:icon="@drawable/ic_plus_circle"
app:iconPadding="13dp"
app:iconTint="@color/riotx_accent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/addByMatrixId" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/knownUsersRecyclerView"
android:layout_width="0dp"
@ -166,10 +131,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addFromPhoneBook"
app:layout_constraintTop_toBottomOf="@+id/userListE2EbyDefaultDisabled"
tools:listitem="@layout/item_known_user" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.checkbox.MaterialCheckBox xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:text="@string/matrix_only_filter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer" />

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageView
android:id="@+id/actionIconImageView"
android:layout_width="44dp"
android:layout_height="44dp"
android:padding="12dp"
android:tint="?riotx_text_secondary"
tools:src="@drawable/ic_invite_people" />
<TextView
android:id="@+id/actionTitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
tools:text="@string/invite_friends" />
</LinearLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/userListHeaderView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="sans-serif-medium"
android:padding="8dp"
android:textColor="?attr/riotx_text_primary"
android:textSize="20sp"
android:textStyle="normal"
tools:text="Recents | Contacts" />

View file

@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_scene_dms_fab_menu"
tools:motionProgress="0.65"
tools:parentTag="androidx.constraintlayout.motion.widget.MotionLayout"
tools:showPaths="true">
<View
android:id="@+id/dmsCreateRoomTouchGuard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_touch_guard_bg"
android:clickable="true"
android:contentDescription="@string/a11y_create_menu_close"
android:focusable="true" />
<!-- Sub menu item 2 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/createDmByQrCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:accessibilityTraversalBefore="@+id/roomListView"
android:contentDescription="@string/a11y_create_direct_message_by_qr_code"
android:src="@drawable/ic_fab_add_by_qr_code"
app:backgroundTint="#FFFFFF"
app:fabCustomSize="48dp"
app:maxImageSize="26dp"
app:tint="@color/black" />
<TextView
android:id="@+id/createDmByQrCodeLabel"
style="@style/VectorLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:importantForAccessibility="no"
android:text="@string/add_by_qr_code" />
<!-- Sub menu item 1 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/createDmByMxid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:accessibilityTraversalBefore="@+id/createDmByQrCode"
android:contentDescription="@string/a11y_create_direct_message_by_mxid"
android:src="@drawable/ic_fab_add_by_mxid"
app:backgroundTint="#FFFFFF"
app:fabCustomSize="48dp"
app:maxImageSize="29dp"
app:tint="@color/black" />
<TextView
android:id="@+id/createDmByMxidLabel"
style="@style/VectorLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:importantForAccessibility="no"
android:text="@string/add_by_matrix_id" />
<!-- Menu -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/dmsCreateRoomButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:accessibilityTraversalBefore="@+id/createDmByMxid"
android:contentDescription="@string/a11y_create_menu_open"
android:src="@drawable/ic_fab_add"
app:maxImageSize="14dp" />
</merge>

View file

@ -79,6 +79,7 @@
<string name="pause_video">Pause</string>
<string name="dismiss">Dismiss</string>
<string name="reset">Reset</string>
<string name="start_chatting">Start Chatting</string>
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
@ -1754,6 +1755,7 @@
<string name="room_filtering_footer_open_room_directory">View the room directory</string>
<string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string>
<string name="user_directory_search_hint">Name or ID (like %s)</string>
<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>
<string name="labs_show_unread_notifications_as_tab">Add a dedicated tab for unread notifications on main screen.</string>
@ -1762,10 +1764,15 @@
<string name="add_by_matrix_id">Add by matrix ID</string>
<string name="add_by_qr_code">Add by QR code</string>
<string name="qr_code">QR code</string>
<string name="creating_direct_room">"Creating room…"</string>
<string name="direct_room_no_known_users">"No result found, use Add by matrix ID to search on server."</string>
<string name="direct_room_start_search">"Start typing to get results"</string>
<string name="direct_room_filter_hint">"Filter by username or ID…"</string>
<string name="direct_room_user_list_recent_title">Recent</string>
<string name="direct_room_user_list_known_title">Known Users</string>
<string name="direct_room_user_list_contacts_title">Contacts</string>
<string name="direct_room_user_list_suggestions_title">Suggestions</string>
<string name="joining_room">"Joining room…"</string>
@ -2540,14 +2547,22 @@
<string name="invite_users_to_room_action_invite">INVITE</string>
<string name="inviting_users_to_room">Inviting users…</string>
<string name="invite_users_to_room_title">Invite Users</string>
<string name="invite_friends">Invite Friends</string>
<string name="invite_friends_text">Hey, Talk to me on Element: %s</string>
<string name="invite_friends_rich_title">🔐️ Join me on element</string>
<string name="invitation_sent_to_one_user">Invitation sent to %1$s</string>
<string name="invitations_sent_to_two_users">Invitations sent to %1$s and %2$s</string>
<string name="not_a_valid_qr_code">"It's not a valid matrix QR code"</string>
<plurals name="invitations_sent_to_one_and_more_users">
<item quantity="one">Invitations sent to %1$s and one more</item>
<item quantity="other">Invitations sent to %1$s and %2$d more</item>
</plurals>
<string name="invite_users_to_room_failure">We could not invite users. Please check the users you want to invite and try again.</string>
<string name="user_code_scan">Scan</string>
<string name="user_code_my_code">My code</string>
<string name="user_code_info_text">This is your matrix.to code. If you share it with someone they can scan it with their element camera to add you as a contact</string>
<string name="choose_locale_current_locale_title">Current language</string>
<string name="choose_locale_other_locales_title">Other available languages</string>
<string name="choose_locale_loading_locales">Loading available languages…</string>
@ -2672,15 +2687,17 @@
<string name="error_opening_banned_room">Can\'t open a room where you are banned from.</string>
<string name="room_error_not_found">Can\'t find this room. Make sure it exists.</string>
<!-- Add by QR code -->
<string name="share_by_text">Share by text</string>
<string name="cannot_dm_self">Cannot DM yourself!</string>
<string name="invalid_qr_code_uri">Invalid QR code (Invalid URI)!</string>
<string name="qr_code_not_scanned">QR code not scanned!</string>
<!-- Universal link -->
<string name="universal_link_malformed">The link was malformed</string>
<string name="warning_room_not_created_yet">The room is not yet created. Cancel the room creation?</string>
<string name="warning_unsaved_change">There are unsaved changes. Discard the changes?</string>
<string name="warning_unsaved_change_discard">Discard changes</string>
<!-- Add by QR code -->
<string name="share_by_text">Share by text</string>
<string name="cannot_dm_self">Cannot DM yourself!</string>
<string name="invalid_qr_code_uri">Invalid QR code (Invalid URI)!</string>
<string name="qr_code_not_scanned">QR code not scanned!</string>
<string name="matrix_to_card_title">Matrix Link</string>
</resources>

View file

@ -1,199 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<!-- Click on main FAB: toggle -->
<Transition
motion:constraintSetEnd="@+id/constraint_set_fab_menu_open"
motion:constraintSetStart="@+id/constraint_set_fab_menu_close"
motion:duration="300"
motion:motionInterpolator="easeInOut">
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/dmsCreateRoomButton" />
<KeyFrameSet>
<!-- First icon goes up quickly to let room for other-->
<KeyPosition
motion:framePosition="50"
motion:keyPositionType="deltaRelative"
motion:motionTarget="@id/createDmByQrCode"
motion:percentX="0.8"
motion:percentY="0.8" />
<KeyPosition
motion:framePosition="50"
motion:keyPositionType="deltaRelative"
motion:motionTarget="@id/createDmByQrCodeLabel"
motion:percentX="0.9"
motion:percentY="0.8" />
<!-- Delay apparition of labels-->
<KeyAttribute
android:alpha="0.4"
motion:framePosition="80"
motion:motionTarget="@id/createDmByMxidLabel" />
<KeyAttribute
android:alpha="0.4"
motion:framePosition="80"
motion:motionTarget="@id/createDmByQrCodeLabel" />
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/constraint_set_fab_menu_close">
<Constraint
android:id="@+id/dmsCreateRoomTouchGuard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_touch_guard_bg"
android:visibility="invisible" />
<!-- Sub menu item 2 -->
<Constraint
android:id="@+id/createDmByQrCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:src="@drawable/ic_fab_add_room"
android:visibility="invisible"
motion:backgroundTint="#FFFFFF"
motion:fabCustomSize="48dp"
motion:layout_constraintBottom_toBottomOf="@+id/dmsCreateRoomButton"
motion:layout_constraintEnd_toEndOf="@+id/dmsCreateRoomButton"
motion:layout_constraintStart_toStartOf="@+id/dmsCreateRoomButton"
motion:layout_constraintTop_toTopOf="@+id/dmsCreateRoomButton"
motion:maxImageSize="26dp"
motion:tint="@color/black" />
<Constraint
android:id="@+id/createDmByQrCodeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/fab_menu_create_room"
android:visibility="invisible"
motion:layout_constraintBottom_toBottomOf="@+id/createDmByQrCode"
motion:layout_constraintEnd_toEndOf="@+id/createDmByQrCode"
motion:layout_constraintTop_toTopOf="@+id/createDmByQrCode" />
<!-- Sub menu item 1 -->
<Constraint
android:id="@+id/createDmByMxid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:src="@drawable/ic_fab_add_chat"
android:visibility="invisible"
motion:backgroundTint="#FFFFFF"
motion:fabCustomSize="48dp"
motion:layout_constraintBottom_toBottomOf="@+id/dmsCreateRoomButton"
motion:layout_constraintEnd_toEndOf="@+id/dmsCreateRoomButton"
motion:layout_constraintStart_toStartOf="@+id/dmsCreateRoomButton"
motion:layout_constraintTop_toTopOf="@+id/dmsCreateRoomButton"
motion:maxImageSize="29dp"
motion:tint="@color/black" />
<Constraint
android:id="@+id/createDmByMxidLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/fab_menu_create_chat"
android:visibility="invisible"
motion:layout_constraintBottom_toBottomOf="@+id/createDmByMxid"
motion:layout_constraintEnd_toEndOf="@+id/createDmByMxid"
motion:layout_constraintTop_toTopOf="@+id/createDmByMxid" />
<!-- Menu -->
<Constraint
android:id="@+id/dmsCreateRoomButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_fab_add"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:maxImageSize="14dp" />
</ConstraintSet>
<ConstraintSet android:id="@+id/constraint_set_fab_menu_open">
<Constraint
android:id="@+id/dmsCreateRoomTouchGuard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_touch_guard_bg" />
<!-- Sub menu item 2 -->
<Constraint
android:id="@+id/createDmByQrCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="14dp"
android:src="@drawable/ic_fab_add_room"
motion:backgroundTint="#FFFFFF"
motion:fabCustomSize="48dp"
motion:layout_constraintBottom_toTopOf="@+id/createDmByMxid"
motion:layout_constraintEnd_toEndOf="@+id/dmsCreateRoomButton"
motion:layout_constraintStart_toStartOf="@+id/dmsCreateRoomButton"
motion:maxImageSize="26dp"
motion:tint="@color/black" />
<Constraint
android:id="@+id/createDmByQrCodeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/fab_menu_create_room"
motion:layout_constraintBottom_toBottomOf="@+id/createDmByQrCode"
motion:layout_constraintEnd_toStartOf="@+id/createDmByQrCode"
motion:layout_constraintTop_toTopOf="@+id/createDmByQrCode" />
<!-- Sub menu item 1 -->
<Constraint
android:id="@+id/createDmByMxid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="25dp"
android:src="@drawable/ic_fab_add_chat"
motion:backgroundTint="#FFFFFF"
motion:fabCustomSize="48dp"
motion:layout_constraintBottom_toTopOf="@+id/dmsCreateRoomButton"
motion:layout_constraintEnd_toEndOf="@+id/dmsCreateRoomButton"
motion:layout_constraintStart_toStartOf="@+id/dmsCreateRoomButton"
motion:maxImageSize="29dp"
motion:tint="@color/black" />
<Constraint
android:id="@+id/createDmByMxidLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/fab_menu_create_chat"
motion:layout_constraintBottom_toBottomOf="@+id/createDmByMxid"
motion:layout_constraintEnd_toStartOf="@+id/createDmByMxid"
motion:layout_constraintTop_toTopOf="@+id/createDmByMxid" />
<!-- Menu -->
<Constraint
android:id="@+id/dmsCreateRoomButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:rotation="135"
android:src="@drawable/ic_fab_add"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:maxImageSize="14dp" />
</ConstraintSet>
</MotionScene>