Merge pull request #2417 from vector-im/feature/bca/quick_invite_dm_tab

Feature/bca/quick invite dm tab
This commit is contained in:
Valere 2020-11-26 14:58:14 +01:00 committed by GitHub
commit 67057bfac4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 2588 additions and 807 deletions

View File

@ -39,7 +39,7 @@ We do not forget all translators, for their work of translating Element into man
Feel free to add your name below, when you contribute to the project!
Name | Matrix ID | GitHub
--------|---------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
Name | Matrix ID | GitHub
----------|-----------------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
TR_SLimey | @tr_slimey:an-atom-in.space | [TR-SLimey](https://github.com/TR-SLimey)

View File

@ -2,7 +2,9 @@ Changes in Element 1.0.11 (2020-XX-XX)
===================================================
Features ✨:
-
- Create DMs with users by scanning their QR code (#2025)
- Add Invite friends quick invite actions (#2348)
- Add friend by scanning QR code, show your code to friends (#2025)
Improvements 🙌:
- New room creation tile with quick action (#2346)
@ -12,6 +14,7 @@ Improvements 🙌:
- Handle events of type "m.room.server_acl" (#890)
- Room creation form: add advanced section to disable federation (#1314)
- Move "Enable Encryption" from room setting screen to room profile screen (#2394)
- Improve Invite user screen (seamless search for matrix ID)
Bugfix 🐛:
- Fix crash on AttachmentViewer (#2365)

View File

@ -27,7 +27,7 @@ interface LoginWizard {
* @param password the password field
* @param deviceName the initial device name
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
* @return a [Cancelable]
*/
fun login(login: String,
password: String,

View File

@ -1204,7 +1204,7 @@ internal class DefaultVerificationService @Inject constructor(
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
?.values?.map { it.deviceId } ?: emptyList()
?.values?.map { it.deviceId }.orEmpty()
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }

View File

@ -103,7 +103,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll()
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
?: emptyList()
.orEmpty()
}
}
}

View File

@ -28,7 +28,7 @@ internal class RoomTypingUsersHandler @Inject constructor(@UserId private val us
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
val roomMemberHelper = RoomMemberHelper(realm, roomId)
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId } ?: emptyList()
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty()
val senderInfo = typingIds.map { userId ->
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
SenderInfo(

View File

@ -37,6 +37,6 @@ internal class DefaultTypingUsersTracker @Inject constructor() : TypingUsersTrac
}
override fun getTypingUsers(roomId: String): List<SenderInfo> {
return typingUsers[roomId] ?: emptyList()
return typingUsers[roomId].orEmpty()
}
}

View File

@ -138,7 +138,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
): LiveData<List<Widget>> {
val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountDataTypes.TYPE_WIDGETS)
return Transformations.map(widgetsAccountData) {
it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList()
it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes).orEmpty()
}
}

View File

@ -310,7 +310,10 @@ class UiAllScreensSanityTest {
clickOn(R.id.createChatRoomButton)
withIdlingResource(activityIdlingResource(CreateDirectRoomActivity::class.java)) {
assertDisplayed(R.id.addByMatrixId)
onView(withId(R.id.userListRecyclerView))
.perform(waitForView(withText(R.string.qr_code)))
onView(withId(R.id.userListRecyclerView))
.perform(waitForView(withText(R.string.invite_friends)))
}
closeSoftKeyboard()

View File

@ -72,7 +72,7 @@
android:id="@+id/debug_qr_code"
android:layout_width="200dp"
android:layout_height="200dp"
tools:src="@tools:sample/avatars" />
tools:src="@drawable/ic_qr_code_add" />
</LinearLayout>

View File

@ -229,6 +229,7 @@
<activity android:name=".features.widgets.WidgetActivity" />
<activity android:name=".features.pin.PinActivity" />
<activity android:name=".features.home.room.detail.search.SearchActivity" />
<activity android:name=".features.usercode.UserCodeActivity" />
<!-- Services -->

View File

@ -111,8 +111,8 @@ import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
import im.vector.app.features.share.IncomingShareFragment
import im.vector.app.features.signout.soft.SoftLogoutFragment
import im.vector.app.features.terms.ReviewTermsFragment
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.usercode.ShowUserCodeFragment
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.widgets.WidgetFragment
@Module
@ -255,13 +255,8 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(UserDirectoryFragment::class)
fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment
@Binds
@IntoMap
@FragmentKey(KnownUsersFragment::class)
fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment
@FragmentKey(UserListFragment::class)
fun bindUserListFragment(fragment: UserListFragment): Fragment
@Binds
@IntoMap
@ -582,4 +577,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(SearchFragment::class)
fun bindSearchFragment(fragment: SearchFragment): Fragment
@Binds
@IntoMap
@FragmentKey(ShowUserCodeFragment::class)
fun bindShowUserCodeFragment(fragment: ShowUserCodeFragment): Fragment
}

View File

@ -50,6 +50,7 @@ import im.vector.app.features.invite.InviteUsersToRoomActivity
import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.link.LinkHandlerActivity
import im.vector.app.features.login.LoginActivity
import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.media.BigImageViewerActivity
import im.vector.app.features.media.VectorAttachmentViewerActivity
import im.vector.app.features.navigation.Navigator
@ -72,6 +73,7 @@ import im.vector.app.features.share.IncomingShareActivity
import im.vector.app.features.signout.soft.SoftLogoutActivity
import im.vector.app.features.terms.ReviewTermsActivity
import im.vector.app.features.ui.UiStateRepository
import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.widgets.WidgetActivity
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment
@ -140,6 +142,7 @@ interface ScreenComponent {
fun inject(activity: VectorAttachmentViewerActivity)
fun inject(activity: VectorJitsiActivity)
fun inject(activity: SearchActivity)
fun inject(activity: UserCodeActivity)
/* ==========================================================================================
* BottomSheets
@ -158,6 +161,7 @@ interface ScreenComponent {
fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(bottomSheet: CallControlsBottomSheet)
fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
fun inject(bottomSheet: MatrixToBottomSheet)
/* ==========================================================================================
* Others

View File

@ -35,7 +35,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.reactions.EmojiChooserViewModel
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
@Module
interface ViewModelModule {
@ -87,8 +87,8 @@ interface ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(UserDirectorySharedActionViewModel::class)
fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel
@ViewModelKey(UserListSharedActionViewModel::class)
fun bindUserListSharedActionViewModel(viewModel: UserListSharedActionViewModel): ViewModel
@Binds
@IntoMap

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.epoxy
import android.widget.CompoundButton
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.checkbox.MaterialCheckBox
import im.vector.app.R
@EpoxyModelClass(layout = R.layout.item_checkbox)
abstract class CheckBoxItem : VectorEpoxyModel<CheckBoxItem.Holder>() {
@EpoxyAttribute
var checked: Boolean = false
@EpoxyAttribute lateinit var title: String
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.checkbox.isChecked = checked
holder.checkbox.text = title
holder.checkbox.setOnCheckedChangeListener(checkChangeListener)
}
class Holder : VectorEpoxyHolder() {
val checkbox by bind<MaterialCheckBox>(R.id.checkbox)
}
}

View File

@ -26,7 +26,7 @@ import androidx.annotation.DrawableRes
import im.vector.app.R
import im.vector.app.core.platform.SimpleTextWatcher
fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter,
fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_search,
@DrawableRes clearIconRes: Int = R.drawable.ic_x_gray) {
addTextChangedListener(object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {

View File

@ -587,6 +587,16 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}
}
fun showSnackbar(message: String, @StringRes withActionTitle: Int?, action: (() -> Unit)?) {
coordinatorLayout?.let {
Snackbar.make(it, message, Snackbar.LENGTH_LONG).apply {
withActionTitle?.let {
setAction(withActionTitle, { action?.invoke() })
}
}.show()
}
}
/* ==========================================================================================
* User Consent
* ========================================================================================== */

View File

@ -29,6 +29,7 @@ import android.os.Build
import android.os.Environment
import android.provider.Browser
import android.provider.MediaStore
import android.provider.Settings
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
@ -448,6 +449,19 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
}
}
fun openAppSettingsPage(activity: Activity) {
try {
activity.startActivity(
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.fromParts("package", activity.packageName, null)
})
} catch (activityNotFoundException: ActivityNotFoundException) {
activity.toast(R.string.error_no_external_application_found)
}
}
/**
* Ask the user to select a location and a file name to write in
*/

View File

@ -30,6 +30,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity
import timber.log.Timber
// Android M permission request code management
@ -284,6 +285,12 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
return isPermissionGranted
}
fun VectorBaseActivity.onPermissionDeniedSnackbar(@StringRes rationaleMessage: Int) {
showSnackbar(getString(rationaleMessage), R.string.settings) {
openAppSettingsPage(this)
}
}
/**
* Helper method used in [.checkPermissions] to populate the list of the
* permissions to be granted (permissionsListToBeGrantedOut) and the list of the permissions already denied (permissionAlreadyDeniedListOut).

View File

@ -136,13 +136,19 @@ fun startSharePlainTextIntent(fragment: Fragment,
activityResultLauncher: ActivityResultLauncher<Intent>?,
chooserTitle: String?,
text: String,
subject: String? = null) {
subject: String? = null,
extraTitle: String? = null) {
val share = Intent(Intent.ACTION_SEND)
share.type = "text/plain"
share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
// Add data to the intent, the receiving app will decide what to do with it.
share.putExtra(Intent.EXTRA_SUBJECT, subject)
share.putExtra(Intent.EXTRA_TEXT, text)
extraTitle?.let {
share.putExtra(Intent.EXTRA_TITLE, it)
}
val intent = Intent.createChooser(share, chooserTitle)
try {
if (activityResultLauncher != null) {

View File

@ -30,10 +30,10 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.UserDirectoryAction
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListAction
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import kotlinx.android.synthetic.main.fragment_contacts_book.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
@ -46,16 +46,16 @@ class ContactsBookFragment @Inject constructor(
) : VectorBaseFragment(), ContactsBookController.Callback {
override fun getLayoutResId() = R.layout.fragment_contacts_book
private val viewModel: UserDirectoryViewModel by activityViewModel()
private val viewModel: UserListViewModel by activityViewModel()
// Use activityViewModel to avoid loading several times the data
private val contactsBookViewModel: ContactsBookViewModel by activityViewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
setupRecyclerView()
setupFilterView()
setupConsentView()
@ -110,7 +110,7 @@ class ContactsBookFragment @Inject constructor(
private fun setupCloseView() {
phoneBookClose.debouncedClicks {
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
}
@ -122,13 +122,13 @@ class ContactsBookFragment @Inject constructor(
override fun onMatrixIdClick(matrixId: String) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
override fun onThreePidClick(threePid: ThreePid) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
}

View File

@ -37,28 +37,31 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.KnownUsersFragmentArgs
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState
import kotlinx.android.synthetic.main.activity.*
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import java.net.HttpURLConnection
import javax.inject.Inject
class CreateDirectRoomActivity : SimpleFragmentActivity() {
class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {
private val viewModel: CreateDirectRoomViewModel by viewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
@ -68,31 +71,34 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
injector.inject(this)
}
override fun create(initialState: UserListViewState): UserListViewModel {
return userListViewModelFactory.create(initialState)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
UserDirectorySharedAction.Close -> finish()
UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
.subscribe { action ->
when (action) {
UserListSharedAction.Close -> finish()
UserListSharedAction.GoBack -> onBackPressed()
is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(action)
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
UserListSharedAction.AddByQrCode -> openAddByQrCode()
}.exhaustive
}
.disposeOnDestroy()
if (isFirstCreation()) {
addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
UserListFragment::class.java,
UserListFragmentArgs(
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room,
isCreatingRoom = true
menuResId = R.menu.vector_create_direct_room
)
)
}
@ -101,6 +107,12 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
}
}
private fun openAddByQrCode() {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA, 0)) {
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
}
}
private fun openPhoneBook() {
// Check permission first
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
@ -116,15 +128,23 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
} else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
}
} else {
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
} else if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
onPermissionDeniedSnackbar(R.string.permissions_denied_add_contact)
}
}
}
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_create_direct_room) {
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(
action.invitees,
action.existingDmRoomId
null
))
}
}
@ -178,6 +198,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
}
companion object {
fun getIntent(context: Context): Intent {
return Intent(context, CreateDirectRoomActivity::class.java)
}

View File

@ -0,0 +1,122 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.createdirect
import android.widget.Toast
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.features.userdirectory.PendingInvitee
import kotlinx.android.synthetic.main.fragment_qr_code_scanner.*
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject
class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragment(), ZXingScannerView.ResultHandler {
private val viewModel: CreateDirectRoomViewModel by activityViewModel()
override fun getLayoutResId() = R.layout.fragment_qr_code_scanner
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
startCamera()
}
}
private fun startCamera() {
// Start camera on resume
scannerView.startCamera()
}
override fun onResume() {
super.onResume()
view?.hideKeyboard()
// Register ourselves as a handler for scan results.
scannerView.setResultHandler(this)
// Start camera on resume
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
}
override fun onPause() {
super.onPause()
// Unregister ourselves as a handler for scan results.
scannerView.setResultHandler(null)
// Stop camera on pause
scannerView.stopCamera()
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
private fun addByQrCode(value: String) {
val mxid = (PermalinkParser.parse(value) as? PermalinkData.UserLink)?.userId
if (mxid === null) {
Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val existingDm = viewModel.session.getExistingDirectRoomWithUser(mxid)
// The following assumes MXIDs are case insensitive
if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) {
Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
// Try to get user from known users and fall back to creating a User object from MXID
val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
viewModel.handle(
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm)
)
}
}
}
override fun handleResult(result: Result?) {
if (result === null) {
Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val rawBytes = getRawBytes(result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
val value = rawBytesStr ?: result.text
addByQrCode(value)
}
}
}

View File

@ -38,7 +38,7 @@ import org.matrix.android.sdk.rx.rx
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
private val rawService: RawService,
private val session: Session)
val session: Session)
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@AssistedInject.Factory

View File

@ -18,15 +18,19 @@ package im.vector.app.features.home
import android.os.Bundle
import android.view.View
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.observeK
import im.vector.app.core.extensions.replaceChildFragment
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.grouplist.GroupListFragment
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.usercode.UserCodeActivity
import im.vector.app.features.workers.signout.SignOutUiWorker
import kotlinx.android.synthetic.main.fragment_home_drawer.*
import org.matrix.android.sdk.api.session.Session
@ -75,6 +79,32 @@ class HomeDrawerFragment @Inject constructor(
SignOutUiWorker(requireActivity()).perform()
}
homeDrawerQRCodeButton.debouncedClicks {
UserCodeActivity.newIntent(requireContext(), sharedActionViewModel.session.myUserId).let {
val options =
ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
homeDrawerHeaderAvatarView,
ViewCompat.getTransitionName(homeDrawerHeaderAvatarView) ?: ""
)
startActivity(it, options.toBundle())
}
}
homeDrawerInviteFriendButton.debouncedClicks {
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = getString(R.string.invite_friends),
text = text,
extraTitle = getString(R.string.invite_friends_rich_title)
)
}
}
// Debug menu
homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode()
homeDrawerHeaderDebugView.debouncedClicks {

View File

@ -17,6 +17,7 @@
package im.vector.app.features.home
import im.vector.app.core.platform.VectorSharedActionViewModel
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>()
class HomeSharedActionViewModel @Inject constructor(val session: Session) : VectorSharedActionViewModel<HomeActivitySharedAction>()

View File

@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.room.list.widget.FabMenuView
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
@EpoxyModelClass(layout = R.layout.item_room_filter_footer)
abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.Holder>() {
@ -46,7 +46,7 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.
val openRoomDirectory by bind<Button>(R.id.roomFilterFooterOpenRoomDirectory)
}
interface FilteredRoomFooterItemListener : FabMenuView.Listener {
interface FilteredRoomFooterItemListener : NotifsFabMenuView.Listener {
fun createRoom(initialName: String)
}
}

View File

@ -45,7 +45,7 @@ import im.vector.app.features.home.room.list.actions.RoomListActionsArgs
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.home.room.list.widget.FabMenuView
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.notifications.NotificationDrawerManager
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.*
@ -66,8 +66,7 @@ class RoomListFragment @Inject constructor(
val roomListViewModelFactory: RoomListViewModel.Factory,
private val notificationDrawerManager: NotificationDrawerManager,
private val sharedViewPool: RecyclerView.RecycledViewPool
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, NotifsFabMenuView.Listener {
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel

View File

@ -22,15 +22,15 @@ import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.isVisible
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.app.R
import kotlinx.android.synthetic.main.motion_fab_menu_merge.view.*
import kotlinx.android.synthetic.main.motion_notifs_fab_menu_merge.view.*
class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
class NotifsFabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : MotionLayout(context, attrs, defStyleAttr) {
var listener: Listener? = null
init {
inflate(context, R.layout.motion_fab_menu_merge, this)
inflate(context, R.layout.motion_notifs_fab_menu_merge, this)
}
override fun onFinishInflate() {

View File

@ -28,7 +28,7 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.UserListFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
@ -50,7 +50,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor(
companion object : MvRxViewModelFactory<HomeServerCapabilitiesViewModel, HomeServerCapabilitiesViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel? {
val fragment: KnownUsersFragment = (viewModelContext as FragmentViewModelContext).fragment()
val fragment: UserListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.homeServerCapabilitiesViewModelFactory.create(state)
}

View File

@ -21,6 +21,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
@ -29,7 +30,6 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
@ -39,12 +39,12 @@ import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.toast
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.userdirectory.KnownUsersFragment
import im.vector.app.features.userdirectory.KnownUsersFragmentArgs
import im.vector.app.features.userdirectory.UserDirectoryFragment
import im.vector.app.features.userdirectory.UserDirectorySharedAction
import im.vector.app.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.app.features.userdirectory.UserDirectoryViewModel
import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
import im.vector.app.features.userdirectory.UserListViewModel
import im.vector.app.features.userdirectory.UserListViewState
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.*
import org.matrix.android.sdk.api.failure.Failure
@ -54,11 +54,11 @@ import javax.inject.Inject
@Parcelize
data class InviteUsersToRoomArgs(val roomId: String) : Parcelable
class InviteUsersToRoomActivity : SimpleFragmentActivity() {
class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {
private val viewModel: InviteUsersToRoomViewModel by viewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
@ -68,32 +68,40 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
injector.inject(this)
}
override fun create(initialState: UserListViewState): UserListViewModel {
return userListViewModelFactory.create(initialState)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
UserDirectorySharedAction.Close -> finish()
UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
}.exhaustive
UserListSharedAction.Close -> finish()
UserListSharedAction.GoBack -> onBackPressed()
is UserListSharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
// not exhaustive because it's a sharedAction
else -> {
}
}
}
.disposeOnDestroy()
if (isFirstCreation()) {
val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG)
addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
UserListFragment::class.java,
UserListFragmentArgs(
title = getString(R.string.invite_users_to_room_title),
menuResId = R.menu.vector_invite_users_to_room,
excludedUserIds = viewModel.getUserIdsOfRoomMembers()
excludedUserIds = viewModel.getUserIdsOfRoomMembers(),
existingRoomId = args?.roomId
)
)
}
@ -101,6 +109,12 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
viewModel.observeViewEvents { renderInviteEvents(it) }
}
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_invite_users_to_room_invite) {
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
}
}
private fun openPhoneBook() {
// Check permission first
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
@ -117,12 +131,8 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
}
}
}
private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_invite_users_to_room_invite) {
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
} else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.matrixto
import android.os.Bundle
import android.view.View
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.bottom_sheet_matrix_to_card.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
class MatrixToBottomSheet(private val matrixItem: MatrixItem) : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var avatarRenderer: AvatarRenderer
interface InteractionListener {
fun didTapStartMessage(matrixItem: MatrixItem)
}
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
private var interactionListener: InteractionListener? = null
override fun getLayoutResId() = R.layout.bottom_sheet_matrix_to_card
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
matrixToCardSendMessageButton.debouncedClicks {
interactionListener?.didTapStartMessage(matrixItem)
dismiss()
}
matrixToCardNameText.setTextOrHide(matrixItem.displayName)
matrixToCardUserIdText.setTextOrHide(matrixItem.id)
avatarRenderer.render(matrixItem, matrixToCardAvatar)
}
companion object {
fun create(matrixItem: MatrixItem, listener: InteractionListener?): MatrixToBottomSheet {
return MatrixToBottomSheet(matrixItem).apply {
interactionListener = listener
}
}
}
}

View File

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

View File

@ -62,7 +62,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
holder.avatarView.isInvisible = directoryAvatarUrl.isNullOrBlank() && includeAllNetworks
holder.nameView.text = directoryName
holder.descritionView.setTextOrHide(directoryDescription)
holder.descriptionView.setTextOrHide(directoryDescription)
}
class Holder : VectorEpoxyHolder() {
@ -70,6 +70,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
val avatarView by bind<ImageView>(R.id.itemRoomDirectoryAvatar)
val nameView by bind<TextView>(R.id.itemRoomDirectoryName)
val descritionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
val descriptionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
}
}

View File

@ -79,6 +79,17 @@ class RoomMemberProfileController @Inject constructor(
divider = false,
action = { callback?.onIgnoreClicked() }
)
if (!state.isMine) {
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction(
id = "direct",
editable = false,
title = stringProvider.getString(R.string.room_member_open_or_create_dm),
dividerColor = dividerColor,
action = { callback?.onOpenDmClicked() }
)
}
}
private fun buildRoomMemberActions(state: RoomMemberProfileViewState) {

View File

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

View File

@ -16,15 +16,13 @@
package im.vector.app.features.settings
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.preference.Preference
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.displayInWebView
import im.vector.app.core.utils.openAppSettingsPage
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.features.version.VersionProvider
import im.vector.app.openOssLicensesMenuActivity
@ -42,18 +40,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
// preference to start the App info screen, to facilitate App permissions access
findPreference<VectorPreference>(APP_INFO_LINK_PREFERENCE_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
activity?.let {
val intent = Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val uri = Uri.fromParts("package", requireContext().packageName, null)
data = uri
}
it.applicationContext.startActivity(intent)
}
activity?.let { openAppSettingsPage(it) }
true
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import android.graphics.Bitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.LuminanceSource
import com.google.zxing.MultiFormatReader
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.ReaderException
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
// Some helper code from BinaryEye
object QRCodeBitmapDecodeHelper {
private val multiFormatReader = MultiFormatReader()
private val decoderHints = mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE))
fun decodeQRFromBitmap(bitmap: Bitmap): Result? =
decode(bitmap, false) ?: decode(bitmap, true)
private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
val pixels = IntArray(bitmap.width * bitmap.height)
return decode(pixels, bitmap, invert)
}
private fun decode(
pixels: IntArray,
bitmap: Bitmap,
invert: Boolean = false
): Result? {
val width = bitmap.width
val height = bitmap.height
if (bitmap.config != Bitmap.Config.ARGB_8888) {
bitmap.copy(Bitmap.Config.ARGB_8888, true)
} else {
bitmap
}.getPixels(pixels, 0, width, 0, 0, width, height)
return decodeLuminanceSource(
RGBLuminanceSource(width, height, pixels),
invert
)
}
private fun decodeLuminanceSource(
source: LuminanceSource,
invert: Boolean
): Result? {
return decodeLuminanceSource(
if (invert) {
source.invert()
} else {
source
}
)
}
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
val bitmap = BinaryBitmap(HybridBinarizer(source))
return try {
multiFormatReader.decode(bitmap, decoderHints)
} catch (e: ReaderException) {
null
} finally {
multiFormatReader.reset()
}
}
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.utils.ImageUtils
import kotlinx.android.synthetic.main.fragment_qr_code_scanner_with_button.*
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject
class ScanUserCodeFragment @Inject constructor()
: VectorBaseFragment(),
ZXingScannerView.ResultHandler {
override fun getLayoutResId() = R.layout.fragment_qr_code_scanner_with_button
val sharedViewModel: UserCodeSharedViewModel by activityViewModel()
var autoFocus = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
userCodeMyCodeButton.debouncedClicks {
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}
userCodeOpenGalleryButton.debouncedClicks {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
}
}
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
startCamera()
} else {
// For now just go back
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}
}
private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
MultiPicker
.get(MultiPicker.IMAGE)
.getSelectedFiles(requireActivity(), activityResult.data)
.firstOrNull()
?.contentUri
?.let { uri ->
// try to see if it is a valid matrix code
val bitmap = ImageUtils.getBitmap(requireContext(), uri)
?: return@let Unit.also {
Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show()
}
handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) })
}
}
}
private fun startCamera() {
userCodeScannerView.startCamera()
userCodeScannerView.setAutoFocus(autoFocus)
userCodeScannerView.debouncedClicks {
this.autoFocus = !autoFocus
userCodeScannerView.setAutoFocus(autoFocus)
}
}
override fun onStart() {
super.onStart()
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
}
override fun onResume() {
super.onResume()
// Register ourselves as a handler for scan results.
userCodeScannerView.setResultHandler(this)
if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)) {
startCamera()
}
}
override fun onPause() {
super.onPause()
userCodeScannerView.setResultHandler(null)
// Stop camera on pause
userCodeScannerView.stopCamera()
}
override fun handleResult(result: Result?) {
if (result === null) {
Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val rawBytes = getRawBytes(result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
val value = rawBytesStr ?: result.text
sharedViewModel.handle(UserCodeActions.DecodedQRCode(value))
}
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.fragment_user_code_show.*
import javax.inject.Inject
class ShowUserCodeFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_user_code_show
val sharedViewModel: UserCodeSharedViewModel by activityViewModel()
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted ->
if (allGranted) {
doOpenQRCodeScanner()
} else {
sharedViewModel.handle(UserCodeActions.CameraPermissionNotGranted)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showUserCodeClose.debouncedClicks {
sharedViewModel.handle(UserCodeActions.DismissAction)
}
showUserCodeScanButton.debouncedClicks {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
doOpenQRCodeScanner()
}
}
showUserCodeShareButton.debouncedClicks {
sharedViewModel.handle(UserCodeActions.ShareByText)
}
sharedViewModel.observeViewEvents {
if (it is UserCodeShareViewEvents.SharePlainText) {
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = it.title,
text = it.text,
extraTitle = it.richPlainText
)
}
}
}
private fun doOpenQRCodeScanner() {
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SCAN))
}
override fun invalidate() = withState(sharedViewModel) { state ->
state.matrixItem?.let { avatarRenderer.render(it, showUserCodeAvatar) }
state.shareLink?.let { showUserCodeQRImage.setData(it) }
showUserCodeCardNameText.setTextOrHide(state.matrixItem?.displayName)
showUserCodeCardUserIdText.setTextOrHide(state.matrixItem?.id)
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.util.MatrixItem
sealed class UserCodeActions : VectorViewModelAction {
object DismissAction : UserCodeActions()
data class SwitchMode(val mode: UserCodeState.Mode) : UserCodeActions()
data class DecodedQRCode(val code: String) : UserCodeActions()
data class StartChattingWithUser(val matrixItem: MatrixItem) : UserCodeActions()
object CameraPermissionNotGranted : UserCodeActions()
object ShareByText : UserCodeActions()
}

View File

@ -0,0 +1,129 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.features.matrixto.MatrixToBottomSheet
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_simple.*
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
import kotlin.reflect.KClass
class UserCodeActivity
: VectorBaseActivity(), UserCodeSharedViewModel.Factory, MatrixToBottomSheet.InteractionListener {
@Inject lateinit var viewModelFactory: UserCodeSharedViewModel.Factory
val sharedViewModel: UserCodeSharedViewModel by viewModel()
@Parcelize
data class Args(
val userId: String
) : Parcelable
override fun getLayoutRes() = R.layout.activity_simple
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isFirstCreation()) {
// should be there early for shared element transition
showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
}
sharedViewModel.selectSubscribe(this, UserCodeState::mode) { mode ->
when (mode) {
UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY)
is UserCodeState.Mode.RESULT -> {
showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
MatrixToBottomSheet.create(mode.matrixItem, this).show(supportFragmentManager, "MatrixToBottomSheet")
}
}
}
sharedViewModel.observeViewEvents {
when (it) {
UserCodeShareViewEvents.Dismiss -> ActivityCompat.finishAfterTransition(this)
UserCodeShareViewEvents.ShowWaitingScreen -> simpleActivityWaitingView.isVisible = true
UserCodeShareViewEvents.HideWaitingScreen -> simpleActivityWaitingView.isVisible = false
is UserCodeShareViewEvents.ToastMessage -> Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
is UserCodeShareViewEvents.NavigateToRoom -> navigator.openRoom(this, it.roomId)
UserCodeShareViewEvents.CameraPermissionNotGranted -> onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
else -> {
}
}
}
}
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
supportFragmentManager.commitTransaction {
setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
replace(R.id.simpleFragmentContainer,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
}
}
}
override fun didTapStartMessage(matrixItem: MatrixItem) {
sharedViewModel.handle(UserCodeActions.StartChattingWithUser(matrixItem))
}
override fun onBackPressed() = withState(sharedViewModel) {
when (it.mode) {
UserCodeState.Mode.SHOW -> super.onBackPressed()
is UserCodeState.Mode.RESULT,
UserCodeState.Mode.SCAN -> sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}.exhaustive
}
override fun create(initialState: UserCodeState) =
viewModelFactory.create(initialState)
companion object {
fun newIntent(context: Context, userId: String): Intent {
return Intent(context, UserCodeActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(userId))
}
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import im.vector.app.core.platform.VectorViewEvents
sealed class UserCodeShareViewEvents : VectorViewEvents {
object Dismiss : UserCodeShareViewEvents()
object ShowWaitingScreen : UserCodeShareViewEvents()
object HideWaitingScreen : UserCodeShareViewEvents()
data class ToastMessage(val message: String) : UserCodeShareViewEvents()
data class NavigateToRoom(val roomId: String) : UserCodeShareViewEvents()
object CameraPermissionNotGranted : UserCodeShareViewEvents()
data class SharePlainText(val text: String, val title: String, val richPlainText: String) : UserCodeShareViewEvents()
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.util.awaitCallback
class UserCodeSharedViewModel @AssistedInject constructor(
@Assisted val initialState: UserCodeState,
private val session: Session,
private val stringProvider: StringProvider,
private val rawService: RawService) : VectorViewModel<UserCodeState, UserCodeActions, UserCodeShareViewEvents>(initialState) {
companion object : MvRxViewModelFactory<UserCodeSharedViewModel, UserCodeState> {
override fun create(viewModelContext: ViewModelContext, state: UserCodeState): UserCodeSharedViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
init {
val user = session.getUser(initialState.userId)
setState {
copy(
matrixItem = user?.toMatrixItem(),
shareLink = session.permalinkService().createPermalink(initialState.userId)
)
}
}
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserCodeState): UserCodeSharedViewModel
}
override fun handle(action: UserCodeActions) {
when (action) {
UserCodeActions.DismissAction -> _viewEvents.post(UserCodeShareViewEvents.Dismiss)
is UserCodeActions.SwitchMode -> setState { copy(mode = action.mode) }
is UserCodeActions.DecodedQRCode -> handleQrCodeDecoded(action)
is UserCodeActions.StartChattingWithUser -> handleStartChatting(action)
UserCodeActions.CameraPermissionNotGranted -> _viewEvents.post(UserCodeShareViewEvents.CameraPermissionNotGranted)
UserCodeActions.ShareByText -> handleShareByText()
}
}
private fun handleShareByText() {
session.permalinkService().createPermalink(session.myUserId)?.let { permalink ->
val text = stringProvider.getString(R.string.invite_friends_text, permalink)
_viewEvents.post(UserCodeShareViewEvents.SharePlainText(
text,
stringProvider.getString(R.string.invite_friends),
stringProvider.getString(R.string.invite_friends_rich_title)
))
}
}
private fun handleStartChatting(withUser: UserCodeActions.StartChattingWithUser) {
val mxId = withUser.matrixItem.id
val existing = session.getExistingDirectRoomWithUser(mxId)
setState {
copy(mode = UserCodeState.Mode.SHOW)
}
if (existing != null) {
// navigate to this room
_viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(existing))
} else {
// we should create the room then navigate
_viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault()
?: true
val roomParams = CreateRoomParams()
.apply {
invitedUserIds.add(mxId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
val roomId =
try {
awaitCallback<String> { session.createRoom(roomParams, it) }
} catch (failure: Throwable) {
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure)))
return@launch
} finally {
_viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
}
_viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId))
}
}
}
private fun handleQrCodeDecoded(action: UserCodeActions.DecodedQRCode) {
val linkedId = PermalinkParser.parse(action.code)
if (linkedId is PermalinkData.FallbackLink) {
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.not_a_valid_qr_code)))
return
}
_viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
viewModelScope.launch(Dispatchers.IO) {
when (linkedId) {
is PermalinkData.RoomLink -> TODO()
is PermalinkData.UserLink -> {
val user = session.getUser(linkedId.userId) ?: awaitCallback<List<User>> {
session.searchUsersDirectory(linkedId.userId, 10, emptySet(), it)
}.firstOrNull { it.userId == linkedId.userId }
// Create raw Uxid in case the user is not searchable
?: User(linkedId.userId, null, null)
setState {
copy(
mode = UserCodeState.Mode.RESULT(user.toMatrixItem())
)
}
}
is PermalinkData.GroupLink -> TODO()
is PermalinkData.FallbackLink -> TODO()
}
_viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.usercode
import com.airbnb.mvrx.MvRxState
import org.matrix.android.sdk.api.util.MatrixItem
data class UserCodeState(
val userId: String,
val matrixItem: MatrixItem? = null,
val shareLink: String? = null,
val mode: Mode = Mode.SHOW
) : MvRxState {
sealed class Mode {
object SHOW : Mode()
object SCAN : Mode()
data class RESULT(val matrixItem: MatrixItem) : Mode()
}
constructor(args: UserCodeActivity.Args) : this(
userId = args.userId
)
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.utils.DebouncedClickListener
@EpoxyModelClass(layout = R.layout.item_contact_action)
abstract class ActionItem : VectorEpoxyModel<ActionItem.Holder>() {
@EpoxyAttribute var title: CharSequence? = null
@EpoxyAttribute @DrawableRes var actionIconRes: Int? = null
@EpoxyAttribute var clickAction: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener(clickAction?.let { DebouncedClickListener(it) })
// If name is empty, use userId as name and force it being centered
holder.actionTitleText.setTextOrHide(title)
if (actionIconRes != null) {
holder.actionTitleImageView.setImageResource(actionIconRes!!)
} else {
holder.actionTitleImageView.setImageDrawable(null)
}
}
class Holder : VectorEpoxyHolder() {
val actionTitleText by bind<TextView>(R.id.actionTitleText)
val actionTitleImageView by bind<ImageView>(R.id.actionIconImageView)
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_contact_detail)
abstract class ContactDetailItem : VectorEpoxyModel<ContactDetailItem.Holder>() {
@EpoxyAttribute lateinit var threePid: String
@EpoxyAttribute var matrixId: String? = null
@EpoxyAttribute var clickListener: ClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.onClick(clickListener)
holder.nameView.text = threePid
holder.matrixIdView.setTextOrHide(matrixId)
}
class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.contactDetailName)
val matrixIdView by bind<TextView>(R.id.contactDetailMatrixId)
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.contacts.MappedContact
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_contact_main)
abstract class ContactItem : VectorEpoxyModel<ContactItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var mappedContact: MappedContact
override fun bind(holder: Holder) {
super.bind(holder)
// If name is empty, use userId as name and force it being centered
holder.nameView.text = mappedContact.displayName
avatarRenderer.render(mappedContact, holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.contactDisplayName)
val avatarImageView by bind<ImageView>(R.id.contactAvatar)
}
}

View File

@ -1,139 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class DirectoryUsersController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() {
private var state: UserDirectoryViewState? = null
var callback: Callback? = null
init {
requestModelBuild()
}
fun setData(state: UserDirectoryViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
val hasSearch = currentState.directorySearchTerm.isNotBlank()
when (val asyncUsers = currentState.directoryUsers) {
is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading()
is Success -> renderSuccess(
computeUsersList(asyncUsers(), currentState.directorySearchTerm),
currentState.getSelectedMatrixId(),
hasSearch
)
is Fail -> renderFailure(asyncUsers.error)
}
}
/**
* Eventually add the searched terms, if it is a userId, and if not already present in the result
*/
private fun computeUsersList(directoryUsers: List<User>, searchTerms: String): List<User> {
return directoryUsers +
searchTerms
.takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } }
?.let { listOf(User(it)) }
.orEmpty()
}
private fun renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderFailure(failure: Throwable) {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(failure))
listener { callback?.retryDirectoryUsersRequest() }
}
}
private fun renderSuccess(users: List<User>,
selectedUsers: List<String>,
hasSearch: Boolean) {
if (users.isEmpty()) {
renderEmptyState(hasSearch)
} else {
renderUsers(users, selectedUsers)
}
}
private fun renderUsers(users: List<User>, selectedUsers: List<String>) {
for (user in users) {
if (user.userId == session.myUserId) {
continue
}
val isSelected = selectedUsers.contains(user.userId)
userDirectoryUserItem {
id(user.userId)
selected(isSelected)
matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(user)
}
}
}
}
private fun renderEmptyState(hasSearch: Boolean) {
val noResultRes = if (hasSearch) {
R.string.no_result_placeholder
} else {
R.string.direct_room_start_search
}
noResultItem {
id("noResult")
text(stringProvider.getString(noResultRes))
}
}
interface Callback {
fun onItemClick(user: User)
fun retryDirectoryUsersRequest()
}
}

View File

@ -1,122 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.EmptyItem_
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.createUIHandler
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class KnownUsersController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider) : PagedListEpoxyController<User>(
modelBuildingHandler = createUIHandler()
) {
private var selectedUsers: List<String> = emptyList()
private var users: Async<List<User>> = Uninitialized
private var isFiltering: Boolean = false
var callback: Callback? = null
init {
requestModelBuild()
}
fun setData(state: UserDirectoryViewState) {
this.isFiltering = !state.filterKnownUsersValue.isEmpty()
val newSelection = state.getSelectedMatrixId()
this.users = state.knownUsers
if (newSelection != selectedUsers) {
this.selectedUsers = newSelection
requestForcedModelBuild()
}
submitList(state.knownUsers())
}
override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> {
return if (item == null) {
EmptyItem_().id(currentPosition)
} else {
val isSelected = selectedUsers.contains(item.userId)
UserDirectoryUserItem_()
.id(item.userId)
.selected(isSelected)
.matrixItem(item.toMatrixItem())
.avatarRenderer(avatarRenderer)
.clickListener { _ ->
callback?.onItemClick(item)
}
}
}
override fun addModels(models: List<EpoxyModel<*>>) {
if (users is Incomplete) {
renderLoading()
} else if (models.isEmpty()) {
renderEmptyState()
} else {
var lastFirstLetter: String? = null
for (model in models) {
if (model is UserDirectoryUserItem) {
if (model.matrixItem.id == session.myUserId) continue
val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName()
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter
UserDirectoryLetterHeaderItem_()
.id(currentFirstLetter)
.letter(currentFirstLetter)
.addIf(showLetter, this)
model.addTo(this)
} else {
continue
}
}
}
}
private fun renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderEmptyState() {
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.direct_room_no_known_users))
}
}
interface Callback {
fun onItemClick(user: User)
}
}

View File

@ -1,94 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setupAsSearch
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_user_directory.*
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject
class UserDirectoryFragment @Inject constructor(
private val directRoomController: DirectoryUsersController
) : VectorBaseFragment(), DirectoryUsersController.Callback {
override fun getLayoutResId() = R.layout.fragment_user_directory
private val viewModel: UserDirectoryViewModel by activityViewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
setupRecyclerView()
setupSearchByMatrixIdView()
setupCloseView()
}
override fun onDestroyView() {
userDirectoryRecyclerView.cleanup()
directRoomController.callback = null
super.onDestroyView()
}
private fun setupRecyclerView() {
directRoomController.callback = this
userDirectoryRecyclerView.configureWith(directRoomController)
}
private fun setupSearchByMatrixIdView() {
userDirectorySearchById.setupAsSearch(searchIconRes = 0)
userDirectorySearchById
.textChanges()
.subscribe {
viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString()))
}
.disposeOnDestroyView()
userDirectorySearchById.showKeyboard(andRequestFocus = true)
}
private fun setupCloseView() {
userDirectoryClose.debouncedClicks {
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
}
}
override fun invalidate() = withState(viewModel) {
directRoomController.setData(it)
}
override fun onItemClick(user: User) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
}
override fun retryDirectoryUsersRequest() {
val currentSearch = userDirectorySearchById.text.toString()
viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch))
}
}

View File

@ -1,153 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import androidx.fragment.app.FragmentActivity
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.invite.InviteUsersToRoomActivity
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String
class UserDirectoryViewModel @AssistedInject constructor(@Assisted
initialState: UserDirectoryViewState,
private val session: Session)
: VectorViewModel<UserDirectoryViewState, UserDirectoryAction, UserDirectoryViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel
}
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
companion object : MvRxViewModelFactory<UserDirectoryViewModel, UserDirectoryViewState> {
override fun create(viewModelContext: ViewModelContext, state: UserDirectoryViewState): UserDirectoryViewModel? {
return when (viewModelContext) {
is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state)
is ActivityViewModelContext -> {
when (viewModelContext.activity<FragmentActivity>()) {
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().userDirectoryViewModelFactory.create(state)
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().userDirectoryViewModelFactory.create(state)
else -> error("Wrong activity or fragment")
}
}
else -> error("Wrong activity or fragment")
}
}
}
init {
observeKnownUsers()
observeDirectoryUsers()
}
override fun handle(action: UserDirectoryAction) {
when (action) {
is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value))
is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value)
is UserDirectoryAction.SelectPendingInvitee -> handleSelectUser(action)
is UserDirectoryAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
}.exhaustive
}
private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemovePendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
setState {
copy(
pendingInvitees = selectedUsers,
existingDmRoomId = getExistingDmRoomId(selectedUsers)
)
}
}
private fun handleSelectUser(action: UserDirectoryAction.SelectPendingInvitee) = withState { state ->
// Reset the filter asap
directoryUsersSearch.accept("")
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
setState {
copy(
pendingInvitees = selectedUsers,
existingDmRoomId = getExistingDmRoomId(selectedUsers)
)
}
}
private fun getExistingDmRoomId(selectedUsers: Set<PendingInvitee>): String? {
return selectedUsers
.takeIf { it.size == 1 }
?.filterIsInstance(PendingInvitee.UserPendingInvitee::class.java)
?.firstOrNull()
?.let { invitee -> session.getExistingDirectRoomWithUser(invitee.user.userId) }
}
private fun observeDirectoryUsers() = withState { state ->
directoryUsersSearch
.debounce(300, TimeUnit.MILLISECONDS)
.switchMapSingle { search ->
val stream = if (search.isBlank()) {
Single.just(emptyList())
} else {
session.rx()
.searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
}
stream.toAsync {
copy(directoryUsers = it, directorySearchTerm = search)
}
}
.subscribe()
.disposeOnClear()
}
private fun observeKnownUsers() = withState { state ->
knownUsersFilter
.throttleLast(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
session.rx().livePagedUsers(it.orNull(), state.excludedUserIds)
}
.execute { async ->
copy(
knownUsers = async,
filterKnownUsersValue = knownUsersFilter.value ?: Option.empty()
)
}
}
}

View File

@ -18,10 +18,10 @@ package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorViewModelAction
sealed class UserDirectoryAction : VectorViewModelAction {
data class FilterKnownUsers(val value: String) : UserDirectoryAction()
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
object ClearFilterKnownUsers : UserDirectoryAction()
data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
sealed class UserListAction : VectorViewModelAction {
data class SearchUsers(val value: String) : UserListAction()
object ClearSearchUsers : UserListAction()
data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
object ComputeMatrixToLinkForSharing : UserListAction()
}

View File

@ -0,0 +1,197 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import android.view.View
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class UserListController @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() {
private var state: UserListViewState? = null
var callback: Callback? = null
fun setData(state: UserListViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
// Build generic items
if (currentState.searchTerm.isBlank()) {
// For now we remove this option if in invite to existing room flow (and not create DM)
if (currentState.pendingInvitees.isEmpty()
// For now we remove this option if in invite to existing room flow (and not create DM)
&& currentState.existingRoomId == null) {
actionItem {
id(R.drawable.ic_share)
title(stringProvider.getString(R.string.invite_friends))
actionIconRes(R.drawable.ic_share)
clickAction(View.OnClickListener {
callback?.onInviteFriendClick()
})
}
}
actionItem {
id(R.drawable.ic_baseline_perm_contact_calendar_24)
title(stringProvider.getString(R.string.contacts_book_title))
actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24)
clickAction(View.OnClickListener {
callback?.onContactBookClick()
})
}
if (currentState.pendingInvitees.isEmpty()
// For now we remove this option if in invite to existing room flow (and not create DM)
&& currentState.existingRoomId == null) {
actionItem {
id(R.drawable.ic_qr_code_add)
title(stringProvider.getString(R.string.qr_code))
actionIconRes(R.drawable.ic_qr_code_add)
clickAction(View.OnClickListener {
callback?.onUseQRCode()
})
}
}
}
when (currentState.knownUsers) {
is Uninitialized -> renderEmptyState()
is Loading -> renderLoading()
is Fail -> renderFailure(currentState.knownUsers.error)
is Success -> buildKnownUsers(currentState, currentState.getSelectedMatrixId())
}
when (val asyncUsers = currentState.directoryUsers) {
is Uninitialized -> {
}
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 }.orEmpty()
)
}
}
private fun buildKnownUsers(currentState: UserListViewState, selectedUsers: List<String>) {
currentState.knownUsers()?.let { userList ->
userListHeaderItem {
id("known_header")
header(stringProvider.getString(R.string.direct_room_user_list_known_title))
}
if (userList.isEmpty()) {
renderEmptyState()
return
}
userList.forEach { item ->
val isSelected = selectedUsers.contains(item.userId)
userDirectoryUserItem {
id(item.userId)
selected(isSelected)
matrixItem(item.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(item)
}
}
}
}
}
private fun buildDirectoryUsers(directoryUsers: List<User>, selectedUsers: List<String>, searchTerms: String, ignoreIds: List<String>) {
val toDisplay = directoryUsers.filter { !ignoreIds.contains(it.userId) }
if (toDisplay.isEmpty() && searchTerms.isBlank()) {
return
}
userListHeaderItem {
id("suggestions")
header(stringProvider.getString(R.string.direct_room_user_list_suggestions_title))
}
if (toDisplay.isEmpty()) {
renderEmptyState()
} else {
toDisplay.forEach { user ->
if (user.userId != session.myUserId) {
val isSelected = selectedUsers.contains(user.userId)
userDirectoryUserItem {
id(user.userId)
selected(isSelected)
matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(user)
}
}
}
}
}
}
private fun renderLoading() {
loadingItem {
id("loading")
}
}
private fun renderEmptyState() {
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.no_result_placeholder))
}
}
private fun renderFailure(failure: Throwable) {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(failure))
}
}
interface Callback {
fun onInviteFriendClick()
fun onContactBookClick()
fun onUseQRCode()
fun onItemClick(user: User)
fun onMatrixIdClick(matrixId: String)
fun onThreePidClick(threePid: ThreePid)
}
}

View File

@ -36,53 +36,64 @@ import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setupAsSearch
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
import kotlinx.android.synthetic.main.fragment_known_users.*
import kotlinx.android.synthetic.main.fragment_user_list.*
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject
class KnownUsersFragment @Inject constructor(
val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory,
private val knownUsersController: KnownUsersController,
class UserListFragment @Inject constructor(
private val userListController: UserListController,
private val dimensionConverter: DimensionConverter,
val homeServerCapabilitiesViewModelFactory: HomeServerCapabilitiesViewModel.Factory
) : VectorBaseFragment(), KnownUsersController.Callback {
) : VectorBaseFragment(), UserListController.Callback {
private val args: KnownUsersFragmentArgs by args()
private val args: UserListFragmentArgs by args()
private val viewModel: UserListViewModel by activityViewModel()
private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
override fun getLayoutResId() = R.layout.fragment_known_users
override fun getLayoutResId() = R.layout.fragment_user_list
override fun getMenuRes() = args.menuResId
private val viewModel: UserDirectoryViewModel by activityViewModel()
private val homeServerCapabilitiesViewModel: HomeServerCapabilitiesViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
userListTitle.text = args.title
vectorBaseActivity.setSupportActionBar(userListToolbar)
knownUsersTitle.text = args.title
vectorBaseActivity.setSupportActionBar(knownUsersToolbar)
setupRecyclerView()
setupFilterView()
setupAddByMatrixIdView()
setupAddFromPhoneBookView()
setupSearchView()
setupCloseView()
homeServerCapabilitiesViewModel.subscribe {
knownUsersE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
}
viewModel.selectSubscribe(this, UserDirectoryViewState::pendingInvitees) {
viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) {
renderSelectedUsers(it)
}
viewModel.observeViewEvents {
when (it) {
is UserListViewEvents.OpenShareMatrixToLing -> {
val text = getString(R.string.invite_friends_text, it.link)
startSharePlainTextIntent(
fragment = this,
activityResultLauncher = null,
chooserTitle = getString(R.string.invite_friends),
text = text,
extraTitle = getString(R.string.invite_friends_rich_title)
)
}
}
}
}
override fun onDestroyView() {
knownUsersController.callback = null
knownUsersRecyclerView.cleanup()
userListRecyclerView.cleanup()
super.onDestroyView()
}
@ -91,69 +102,52 @@ class KnownUsersFragment @Inject constructor(
val showMenuItem = it.pendingInvitees.isNotEmpty()
menu.forEach { menuItem ->
menuItem.isVisible = showMenuItem
if (args.isCreatingRoom) {
menuItem.setTitle(if (it.existingDmRoomId != null) R.string.action_open else R.string.create_room_action_create)
}
}
}
super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(
item.itemId,
it.pendingInvitees,
it.existingDmRoomId
))
sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees))
return@withState true
}
private fun setupAddByMatrixIdView() {
addByMatrixId.debouncedClicks {
sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory)
}
}
private fun setupAddFromPhoneBookView() {
addFromPhoneBook.debouncedClicks {
// TODO handle Permission first
sharedActionViewModel.post(UserDirectorySharedAction.OpenPhoneBook)
}
}
private fun setupRecyclerView() {
knownUsersController.callback = this
userListController.callback = this
// Don't activate animation as we might have way to much item animation when filtering
knownUsersRecyclerView.configureWith(knownUsersController, disableItemAnimation = true)
userListRecyclerView.configureWith(userListController, disableItemAnimation = true)
}
private fun setupFilterView() {
knownUsersFilter
private fun setupSearchView() {
withState(viewModel) {
userListSearch.hint = getString(R.string.user_directory_search_hint)
}
userListSearch
.textChanges()
.startWith(knownUsersFilter.text)
.startWith(userListSearch.text)
.subscribe { text ->
val filterValue = text.trim()
val action = if (filterValue.isBlank()) {
UserDirectoryAction.ClearFilterKnownUsers
val searchValue = text.trim()
val action = if (searchValue.isBlank()) {
UserListAction.ClearSearchUsers
} else {
UserDirectoryAction.FilterKnownUsers(filterValue.toString())
UserListAction.SearchUsers(searchValue.toString())
}
viewModel.handle(action)
}
.disposeOnDestroyView()
knownUsersFilter.setupAsSearch()
knownUsersFilter.requestFocus()
userListSearch.setupAsSearch()
userListSearch.requestFocus()
}
private fun setupCloseView() {
knownUsersClose.debouncedClicks {
userListClose.debouncedClicks {
requireActivity().finish()
}
}
override fun invalidate() = withState(viewModel) {
knownUsersController.setData(it)
userListController.setData(it)
}
private fun renderSelectedUsers(invitees: Set<PendingInvitee>) {
@ -183,12 +177,35 @@ class KnownUsersFragment @Inject constructor(
chip.isCloseIconVisible = true
chipGroup.addView(chip)
chip.setOnCloseIconClickListener {
viewModel.handle(UserDirectoryAction.RemovePendingInvitee(pendingInvitee))
viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee))
}
}
override fun onInviteFriendClick() {
viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing)
}
override fun onContactBookClick() {
sharedActionViewModel.post(UserListSharedAction.OpenPhoneBook)
}
override fun onItemClick(user: User) {
view?.hideKeyboard()
viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
}
override fun onMatrixIdClick(matrixId: String) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
}
override fun onThreePidClick(threePid: ThreePid) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
}
override fun onUseQRCode() {
view?.hideKeyboard()
sharedActionViewModel.post(UserListSharedAction.AddByQrCode)
}
}

View File

@ -20,9 +20,9 @@ import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class KnownUsersFragmentArgs(
data class UserListFragmentArgs(
val title: String,
val menuResId: Int,
val excludedUserIds: Set<String>? = null,
val isCreatingRoom: Boolean = false
val existingRoomId: String? = null
) : Parcelable

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.userdirectory
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_user_list_header)
abstract class UserListHeaderItem : VectorEpoxyModel<UserListHeaderItem.Holder>() {
@EpoxyAttribute var header: String = ""
override fun bind(holder: Holder) {
super.bind(holder)
holder.headerTextView.text = header
}
class Holder : VectorEpoxyHolder() {
val headerTextView by bind<TextView>(R.id.userListHeaderView)
}
}

View File

@ -18,12 +18,10 @@ package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorSharedAction
sealed class UserDirectorySharedAction : VectorSharedAction {
object OpenUsersDirectory : UserDirectorySharedAction()
object OpenPhoneBook : UserDirectorySharedAction()
object Close : UserDirectorySharedAction()
object GoBack : UserDirectorySharedAction()
data class OnMenuItemSelected(val itemId: Int,
val invitees: Set<PendingInvitee>,
val existingDmRoomId: String?) : UserDirectorySharedAction()
sealed class UserListSharedAction : VectorSharedAction {
object Close : UserListSharedAction()
object GoBack : UserListSharedAction()
data class OnMenuItemSelected(val itemId: Int, val invitees: Set<PendingInvitee>) : UserListSharedAction()
object OpenPhoneBook : UserListSharedAction()
object AddByQrCode : UserListSharedAction()
}

View File

@ -19,4 +19,4 @@ package im.vector.app.features.userdirectory
import im.vector.app.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserDirectorySharedAction>()
class UserListSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserListSharedAction>()

View File

@ -21,4 +21,6 @@ import im.vector.app.core.platform.VectorViewEvents
/**
* Transient events for invite users to room screen
*/
sealed class UserDirectoryViewEvents : VectorViewEvents
sealed class UserListViewEvents : VectorViewEvents {
data class OpenShareMatrixToLing(val link: String) : UserListViewEvents()
}

View File

@ -0,0 +1,180 @@
/*
* 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.Optional
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
private typealias KnownUsersSearch = String
private typealias DirectoryUsersSearch = String
class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
private val session: Session)
: VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {
private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
private var currentUserSearchDisposable: Disposable? = null
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserListViewState): UserListViewModel
}
companion object : MvRxViewModelFactory<UserListViewModel, UserListViewState> {
override fun create(viewModelContext: ViewModelContext, state: UserListViewState): UserListViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
init {
setState {
copy(
myUserId = session.myUserId,
existingRoomId = initialState.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<User>())
} else {
val searchObservable = session.rx()
.searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet())
.map { users ->
users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
// 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
if (!MatrixPatterns.isUserId(search)) {
searchObservable
} else {
val profileObservable = session.rx().getProfileInfo(search)
.map { json ->
User(
userId = search,
displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
).toOptional()
}
.onErrorReturn { Optional.empty() }
Single.zip(searchObservable, profileObservable, { searchResults, optionalProfile ->
val profile = optionalProfile.getOrNull() ?: return@zip searchResults
val searchContainsProfile = searchResults.indexOfFirst { it.userId == profile.userId } != -1
if (searchContainsProfile) {
searchResults
} else {
listOf(profile) + searchResults
}
})
}
}
stream.toAsync {
copy(directoryUsers = it)
}
}
.subscribe()
.disposeOnClear()
}
private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
}
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
}
}

View File

@ -17,30 +17,33 @@
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)
constructor(args: UserListFragmentArgs) : this(
existingRoomId = args.existingRoomId
)
fun getSelectedMatrixId(): List<String> {
return pendingInvitees
.mapNotNull {
when (it) {
is PendingInvitee.UserPendingInvitee -> it.user.userId
is PendingInvitee.UserPendingInvitee -> it.user.userId
is PendingInvitee.ThreePidPendingInvitee -> null
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,18L6,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1z"/>
</vector>

View File

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

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="37dp"
android:height="36dp"
android:viewportWidth="37"
android:viewportHeight="36">
<path
android:pathData="M17.5911,26.2922C15.9951,27.3704 14.0711,28 12,28C9.7488,28 7.6713,27.2561 6,26.0007C3.5711,24.1763 2,21.2716 2,18C2,12.4772 6.4771,8 12,8C17.5228,8 22,12.4772 22,18C22,21.4518 20.2511,24.4951 17.5911,26.2922ZM12,18.5C13.6569,18.5 15,17.0449 15,15.25C15,13.4551 13.6569,12 12,12C10.3431,12 9,13.4551 9,15.25C9,17.0449 10.3431,18.5 12,18.5ZM12,26C14.162,26 16.1236,25.1424 17.5634,23.7488C16.673,21.5506 14.5176,20 12,20C9.4824,20 7.327,21.5506 6.4366,23.7488C7.8763,25.1424 9.838,26 12,26Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M17.5911,26.2922C15.9951,27.3704 14.0711,28 12,28C9.7488,28 7.6713,27.2561 6,26.0007C3.5711,24.1763 2,21.2716 2,18C2,12.4772 6.4771,8 12,8C17.5228,8 22,12.4772 22,18C22,21.4518 20.2511,24.4951 17.5911,26.2922ZM12,18.5C13.6569,18.5 15,17.0449 15,15.25C15,13.4551 13.6569,12 12,12C10.3431,12 9,13.4551 9,15.25C9,17.0449 10.3431,18.5 12,18.5ZM12,26C14.162,26 16.1236,25.1424 17.5634,23.7488C16.673,21.5506 14.5176,20 12,20C9.4824,20 7.327,21.5506 6.4366,23.7488C7.8763,25.1424 9.838,26 12,26Z"
android:fillType="evenOdd"/>
<path
android:pathData="M17.5911,26.2922L16.4715,24.6349L17.5911,26.2922ZM6,26.0007L4.7989,27.5999L4.7989,27.5999L6,26.0007ZM17.5634,23.7488L18.9544,25.1859L19.9234,24.2479L19.4171,22.998L17.5634,23.7488ZM6.4366,23.7488L4.5829,22.998L4.0766,24.2479L5.0456,25.1859L6.4366,23.7488ZM12,30C14.4825,30 16.7945,29.244 18.7107,27.9494L16.4715,24.6349C15.1957,25.4968 13.6596,26 12,26V30ZM4.7989,27.5999C6.8046,29.1065 9.3008,30 12,30V26C10.1967,26 8.538,25.4058 7.2011,24.4016L4.7989,27.5999ZM0,18C0,21.9273 1.8887,25.414 4.7989,27.5999L7.2011,24.4016C5.2535,22.9387 4,20.616 4,18H0ZM12,6C5.3726,6 0,11.3726 0,18H4C4,13.5817 7.5817,10 12,10V6ZM24,18C24,11.3726 18.6274,6 12,6V10C16.4183,10 20,13.5817 20,18H24ZM18.7107,27.9494C21.8977,25.7963 24,22.144 24,18H20C20,20.7596 18.6045,23.1939 16.4715,24.6349L18.7107,27.9494ZM13,15.25C13,16.0941 12.4046,16.5 12,16.5V20.5C14.9091,20.5 17,17.9958 17,15.25H13ZM12,14C12.4046,14 13,14.4059 13,15.25H17C17,12.5042 14.9091,10 12,10V14ZM11,15.25C11,14.4059 11.5954,14 12,14V10C9.0909,10 7,12.5042 7,15.25H11ZM12,16.5C11.5954,16.5 11,16.0941 11,15.25H7C7,17.9958 9.0909,20.5 12,20.5V16.5ZM16.1724,22.3118C15.0906,23.3588 13.6223,24 12,24V28C14.7017,28 17.1567,26.926 18.9544,25.1859L16.1724,22.3118ZM12,22C13.6752,22 15.1146,23.0305 15.7097,24.4996L19.4171,22.998C18.2314,20.0707 15.3599,18 12,18V22ZM8.2903,24.4996C8.8854,23.0305 10.3248,22 12,22V18C8.6401,18 5.7686,20.0707 4.5829,22.998L8.2903,24.4996ZM12,24C10.3777,24 8.9094,23.3588 7.8276,22.3118L5.0456,25.1859C6.8433,26.926 9.2983,28 12,28V24Z"
android:fillColor="#000000"/>
</group>
<path
android:pathData="M27,18H35"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M31,14L31,22"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="37dp"
android:height="36dp"
android:viewportWidth="37"
android:viewportHeight="36">
<path
android:pathData="M17.5911,26.2922C15.9951,27.3704 14.0711,28 12,28C9.7488,28 7.6713,27.2561 6,26.0007C3.5711,24.1763 2,21.2716 2,18C2,12.4772 6.4771,8 12,8C17.5228,8 22,12.4772 22,18C22,21.4518 20.2511,24.4951 17.5911,26.2922ZM12,18.5C13.6569,18.5 15,17.0449 15,15.25C15,13.4551 13.6569,12 12,12C10.3431,12 9,13.4551 9,15.25C9,17.0449 10.3431,18.5 12,18.5ZM12,26C14.162,26 16.1236,25.1424 17.5634,23.7488C16.673,21.5506 14.5176,20 12,20C9.4824,20 7.327,21.5506 6.4366,23.7488C7.8763,25.1424 9.838,26 12,26Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M17.5911,26.2922C15.9951,27.3704 14.0711,28 12,28C9.7488,28 7.6713,27.2561 6,26.0007C3.5711,24.1763 2,21.2716 2,18C2,12.4772 6.4771,8 12,8C17.5228,8 22,12.4772 22,18C22,21.4518 20.2511,24.4951 17.5911,26.2922ZM12,18.5C13.6569,18.5 15,17.0449 15,15.25C15,13.4551 13.6569,12 12,12C10.3431,12 9,13.4551 9,15.25C9,17.0449 10.3431,18.5 12,18.5ZM12,26C14.162,26 16.1236,25.1424 17.5634,23.7488C16.673,21.5506 14.5176,20 12,20C9.4824,20 7.327,21.5506 6.4366,23.7488C7.8763,25.1424 9.838,26 12,26Z"
android:fillType="evenOdd"/>
<path
android:pathData="M17.5911,26.2922L16.4715,24.6349L17.5911,26.2922ZM6,26.0007L4.7989,27.5999L4.7989,27.5999L6,26.0007ZM17.5634,23.7488L18.9544,25.1859L19.9234,24.2479L19.4171,22.998L17.5634,23.7488ZM6.4366,23.7488L4.5829,22.998L4.0766,24.2479L5.0456,25.1859L6.4366,23.7488ZM12,30C14.4825,30 16.7945,29.244 18.7107,27.9494L16.4715,24.6349C15.1957,25.4968 13.6596,26 12,26V30ZM4.7989,27.5999C6.8046,29.1065 9.3008,30 12,30V26C10.1967,26 8.538,25.4058 7.2011,24.4016L4.7989,27.5999ZM0,18C0,21.9273 1.8887,25.414 4.7989,27.5999L7.2011,24.4016C5.2535,22.9387 4,20.616 4,18H0ZM12,6C5.3726,6 0,11.3726 0,18H4C4,13.5817 7.5817,10 12,10V6ZM24,18C24,11.3726 18.6274,6 12,6V10C16.4183,10 20,13.5817 20,18H24ZM18.7107,27.9494C21.8977,25.7963 24,22.144 24,18H20C20,20.7596 18.6045,23.1939 16.4715,24.6349L18.7107,27.9494ZM13,15.25C13,16.0941 12.4046,16.5 12,16.5V20.5C14.9091,20.5 17,17.9958 17,15.25H13ZM12,14C12.4046,14 13,14.4059 13,15.25H17C17,12.5042 14.9091,10 12,10V14ZM11,15.25C11,14.4059 11.5954,14 12,14V10C9.0909,10 7,12.5042 7,15.25H11ZM12,16.5C11.5954,16.5 11,16.0941 11,15.25H7C7,17.9958 9.0909,20.5 12,20.5V16.5ZM16.1724,22.3118C15.0906,23.3588 13.6223,24 12,24V28C14.7017,28 17.1567,26.926 18.9544,25.1859L16.1724,22.3118ZM12,22C13.6752,22 15.1146,23.0305 15.7097,24.4996L19.4171,22.998C18.2314,20.0707 15.3599,18 12,18V22ZM8.2903,24.4996C8.8854,23.0305 10.3248,22 12,22V18C8.6401,18 5.7686,20.0707 4.5829,22.998L8.2903,24.4996ZM12,24C10.3777,24 8.9094,23.3588 7.8276,22.3118L5.0456,25.1859C6.8433,26.926 9.2983,28 12,28V24Z"
android:fillColor="#000000"/>
</group>
<path
android:pathData="M27,18H35"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M31,14L31,22"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View File

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

View File

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

View File

@ -0,0 +1,36 @@
<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="M15,3L20,3A1,1 0,0 1,21 4L21,9A1,1 0,0 1,20 10L15,10A1,1 0,0 1,14 9L14,4A1,1 0,0 1,15 3z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M14.25,16.5L20.75,16.5A0.25,0.25 0,0 1,21 16.75L21,18.25A0.25,0.25 0,0 1,20.75 18.5L14.25,18.5A0.25,0.25 0,0 1,14 18.25L14,16.75A0.25,0.25 0,0 1,14.25 16.5z"
android:fillColor="#000000"/>
<path
android:pathData="M18.5,14.25L18.5,20.75A0.25,0.25 0,0 1,18.25 21L16.75,21A0.25,0.25 0,0 1,16.5 20.75L16.5,14.25A0.25,0.25 0,0 1,16.75 14L18.25,14A0.25,0.25 0,0 1,18.5 14.25z"
android:fillColor="#000000"/>
<path
android:pathData="M4,14L9,14A1,1 0,0 1,10 15L10,20A1,1 0,0 1,9 21L4,21A1,1 0,0 1,3 20L3,15A1,1 0,0 1,4 14z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M4,3L9,3A1,1 0,0 1,10 4L10,9A1,1 0,0 1,9 10L4,10A1,1 0,0 1,3 9L3,4A1,1 0,0 1,4 3z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
<path
android:pathData="M5.75,5.5L7.25,5.5A0.25,0.25 0,0 1,7.5 5.75L7.5,7.25A0.25,0.25 0,0 1,7.25 7.5L5.75,7.5A0.25,0.25 0,0 1,5.5 7.25L5.5,5.75A0.25,0.25 0,0 1,5.75 5.5z"
android:fillColor="#000000"/>
<path
android:pathData="M5.75,16.5L7.25,16.5A0.25,0.25 0,0 1,7.5 16.75L7.5,18.25A0.25,0.25 0,0 1,7.25 18.5L5.75,18.5A0.25,0.25 0,0 1,5.5 18.25L5.5,16.75A0.25,0.25 0,0 1,5.75 16.5z"
android:fillColor="#000000"/>
<path
android:pathData="M16.75,5.5L18.25,5.5A0.25,0.25 0,0 1,18.5 5.75L18.5,7.25A0.25,0.25 0,0 1,18.25 7.5L16.75,7.5A0.25,0.25 0,0 1,16.5 7.25L16.5,5.75A0.25,0.25 0,0 1,16.75 5.5z"
android:fillColor="#000000"/>
</vector>

View File

@ -1,26 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/vector_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/VectorToolbarStyle"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent" />
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
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_toBottomOf="@id/toolbar" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/merge_overlay_waiting_view"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
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_toBottomOf="@id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/merge_overlay_waiting_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/vector_coordinator_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -10,4 +10,21 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
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:layout_width="40dp"
android:layout_height="40dp" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,66 @@
<?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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_margin="8dp">
<im.vector.app.core.ui.views.QrCodeImageView
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="@drawable/ic_qr_code_add" />
</FrameLayout>

View File

@ -31,10 +31,11 @@
<ImageView
android:id="@+id/homeDrawerHeaderAvatarView"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="24dp"
android:transitionName="profile"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
@ -43,13 +44,13 @@
android:id="@+id/homeDrawerUsernameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:singleLine="true"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/homeDrawerQRCodeButton"
app:layout_constraintStart_toStartOf="@+id/homeDrawerHeaderAvatarView"
app:layout_constraintTop_toBottomOf="@+id/homeDrawerHeaderAvatarView"
tools:text="@sample/matrix.json/data/displayName" />
@ -58,18 +59,67 @@
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_share"
app:iconGravity="textStart"
app:iconSize="20dp"
app:iconTint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/homeDrawerUserIdView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.fragment.app.FragmentContainerView

View File

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

View File

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

View File

@ -14,12 +14,12 @@
android:overScrollMode="always"
tools:listitem="@layout/item_room" />
<im.vector.app.features.home.room.list.widget.FabMenuView
<im.vector.app.features.home.room.list.widget.NotifsFabMenuView
android:id="@+id/createChatFabMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layoutDescription="@xml/motion_scene_fab_menu"
app:layoutDescription="@xml/motion_scene_notifs_fab_menu"
tools:showPaths="true"
tools:visibility="visible" />

View File

@ -0,0 +1,198 @@
<?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:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:elevation="4dp"
android:transitionName="profile"
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/showUserCodeShareButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minWidth="130dp"
android:text="@string/user_code_share"
app:icon="@drawable/ic_share"
app:iconTint="@color/white"
app:layout_constraintBottom_toTopOf="@id/showUserCodeScanButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/showUserCodeInfoText"
app:layout_constraintVertical_bias="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/showUserCodeScanButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
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/showUserCodeShareButton"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -10,7 +10,7 @@
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/knownUsersToolbar"
android:id="@+id/userListToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
@ -24,7 +24,7 @@
android:layout_height="match_parent">
<ImageView
android:id="@+id/knownUsersClose"
android:id="@+id/userListClose"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:clickable="true"
@ -37,7 +37,7 @@
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/knownUsersTitle"
android:id="@+id/userListTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
@ -51,7 +51,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/knownUsersClose"
app:layout_constraintStart_toEndOf="@+id/userListClose"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -67,7 +67,7 @@
android:layout_marginEnd="@dimen/layout_horizontal_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/knownUsersToolbar"
app:layout_constraintTop_toBottomOf="@+id/userListToolbar"
app:maxHeight="64dp">
<com.google.android.material.chip.ChipGroup
@ -79,7 +79,7 @@
</im.vector.app.core.platform.MaxHeightScrollView>
<EditText
android:id="@+id/knownUsersFilter"
android:id="@+id/userListSearch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
@ -87,28 +87,29 @@
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"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
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,46 +118,11 @@
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:id="@+id/userListRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:fastScrollEnabled="true"
@ -166,10 +132,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addFromPhoneBook"
app:layout_constraintTop_toBottomOf="@+id/userListE2EbyDefaultDisabled"
tools:listitem="@layout/item_known_user" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,14 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
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"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
tools:text="@string/matrix_only_filter" />

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:padding="10dp"
app:tint="?riotx_text_secondary"
tools:src="@drawable/ic_invite_people" />
<TextView
android:id="@+id/actionTitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
tools:text="@string/invite_friends" />
</LinearLayout>

View File

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

View File

@ -11,6 +11,6 @@
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
android:contentDescription="@string/a11y_qr_code_for_verification"
tools:src="@color/riotx_header_panel_background_black" />
tools:src="@drawable/ic_qr_code_add" />
</FrameLayout>

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_scene_fab_menu"
app:layoutDescription="@xml/motion_scene_notifs_fab_menu"
tools:motionProgress="0.65"
tools:parentTag="androidx.constraintlayout.motion.widget.MotionLayout"
tools:showPaths="true">

View File

@ -79,6 +79,7 @@
<string name="pause_video">Pause</string>
<string name="dismiss">Dismiss</string>
<string name="reset">Reset</string>
<string name="start_chatting">Start Chatting</string>
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
@ -424,6 +425,8 @@
<string name="permissions_msg_contacts_warning_other_androids">Element can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?</string>
<string name="permissions_action_not_performed_missing_permissions">Sorry. Action not performed, due to missing permissions</string>
<string name="permissions_denied_qr_code">To scan a QR code, you need to allow camera access.</string>
<string name="permissions_denied_add_contact">Allow permission to access your contacts.</string>
<!-- medias slider string -->
<string name="media_slider_saved">Saved</string>
@ -1754,6 +1757,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">Search by name or ID</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>
@ -1761,10 +1765,16 @@
<string name="link_copied_to_clipboard">Link copied to clipboard</string>
<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>
@ -1828,6 +1838,8 @@
<string name="a11y_create_menu_open">Open the create room menu</string>
<string name="a11y_create_menu_close">Close the create room menu…</string>
<string name="a11y_create_direct_message">Create a new direct conversation</string>
<string name="a11y_create_direct_message_by_mxid">Create a new direct conversation by Matrix ID</string>
<string name="a11y_create_direct_message_by_qr_code">Create a new direct conversation by scanning a QR code</string>
<string name="a11y_create_room">Create a new room</string>
<string name="a11y_close_keys_backup_banner">Close keys backup banner</string>
<string name="a11y_show_password">Show password</string>
@ -2537,14 +2549,23 @@
<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 a QR code</string>
<string name="user_code_share">Share my code</string>
<string name="user_code_my_code">My code</string>
<string name="user_code_info_text">Share this code with people so they can scan it to add you and start chatting.</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>
@ -2669,9 +2690,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>
<string name="matrix_to_card_title">Matrix Link</string>
</resources>

View File

@ -2,13 +2,15 @@
<resources>
<style name="VectorSnackBarStyle" parent="@style/Widget.MaterialComponents.Snackbar">
<item name="android:background">@color/notification_accent_color</item>
<!-- <item name="android:background">@color/notification_accent_color</item>-->
</style>
<style name="VectorSnackBarButton" parent="@style/Widget.MaterialComponents.Button" />
<style name="VectorSnackBarButton" parent="@style/Widget.MaterialComponents.Button.TextButton">
<!-- <item name="android:textColor">@color/white</item>-->
</style>
<style name="VectorSnackBarText" parent="@style/Widget.MaterialComponents.Snackbar.TextView">
<item name="android:textColor">@color/white</item>
<!-- <item name="android:textColor">@color/white</item>-->
</style>
</resources>