mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
QR code invite flow support - invite friends
Author: Valere
This commit is contained in:
parent
e8d084b855
commit
1070c23608
65 changed files with 2311 additions and 1188 deletions
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,38 +71,37 @@ 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 = viewModelProvider.get(UserListSharedActionViewModel::class.java)
|
||||
sharedActionViewModel
|
||||
.observe()
|
||||
.subscribe { sharedAction ->
|
||||
when (sharedAction) {
|
||||
UserDirectorySharedAction.OpenUsersDirectory ->
|
||||
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
|
||||
UserDirectorySharedAction.Close -> finish()
|
||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
||||
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
|
||||
.subscribe { action ->
|
||||
when (action) {
|
||||
UserListSharedAction.Close -> finish()
|
||||
UserListSharedAction.GoBack -> onBackPressed()
|
||||
is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(action)
|
||||
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
|
||||
UserListSharedAction.AddByQrCode -> openAddByQrCode()
|
||||
}.exhaustive
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
if (isFirstCreation()) {
|
||||
addFragment(
|
||||
R.id.container,
|
||||
KnownUsersFragment::class.java,
|
||||
KnownUsersFragmentArgs(
|
||||
UserListFragment::class.java,
|
||||
UserListFragmentArgs(
|
||||
title = getString(R.string.fab_menu_create_chat),
|
||||
menuResId = R.menu.vector_create_direct_room,
|
||||
isCreatingRoom = true
|
||||
menuResId = R.menu.vector_create_direct_room
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,8 +93,6 @@ 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()
|
||||
|
@ -84,13 +102,9 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen
|
|||
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)))
|
||||
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
navigator.openRoom(requireContext(), existingDm, null, false)
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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>()
|
|
@ -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()
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -17,25 +17,24 @@
|
|||
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 {
|
21
vector/src/main/res/drawable/ic_book.xml
Normal file
21
vector/src/main/res/drawable/ic_book.xml
Normal 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>
|
10
vector/src/main/res/drawable/ic_invite_people.xml
Normal file
10
vector/src/main/res/drawable/ic_invite_people.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="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>
|
29
vector/src/main/res/drawable/ic_picture_icon.xml
Normal file
29
vector/src/main/res/drawable/ic_picture_icon.xml
Normal 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>
|
72
vector/src/main/res/drawable/ic_qr_code_add.xml
Normal file
72
vector/src/main/res/drawable/ic_qr_code_add.xml
Normal 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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
70
vector/src/main/res/layout/fragment_matrix_to_card.xml
Normal file
70
vector/src/main/res/layout/fragment_matrix_to_card.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
183
vector/src/main/res/layout/fragment_user_code_show.xml
Normal file
183
vector/src/main/res/layout/fragment_user_code_show.xml
Normal 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>
|
|
@ -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>
|
13
vector/src/main/res/layout/item_checkbox.xml
Normal file
13
vector/src/main/res/layout/item_checkbox.xml
Normal 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" />
|
31
vector/src/main/res/layout/item_contact_action.xml
Normal file
31
vector/src/main/res/layout/item_contact_action.xml
Normal 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>
|
14
vector/src/main/res/layout/item_user_list_header.xml
Normal file
14
vector/src/main/res/layout/item_user_list_header.xml
Normal 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" />
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue