Add ability to share profile by QR code

This commit is contained in:
TR-SLimey 2020-11-06 13:04:21 +00:00 committed by Valere
parent 5b278f704c
commit e8d084b855
22 changed files with 680 additions and 71 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,7 @@ Changes in Element 1.0.11 (2020-XX-XX)
===================================================
Features ✨:
-
- Create DMs with users by scanning their QR code (#2025)
Improvements 🙌:
- New room creation tile with quick action (#2346)

View file

@ -22,6 +22,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
@ -37,6 +38,8 @@ 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
@ -72,35 +75,45 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::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
}
.disposeOnDestroy()
if (isFirstCreation()) {
addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room,
isCreatingRoom = true
)
)
if (intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
if (isFirstCreation()) { openAddByQrCode() }
} else {
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
UserDirectorySharedAction.Close -> finish()
UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
}.exhaustive
}
.disposeOnDestroy()
if (isFirstCreation()) {
addFragment(
R.id.container,
KnownUsersFragment::class.java,
KnownUsersFragmentArgs(
title = getString(R.string.fab_menu_create_chat),
menuResId = R.menu.vector_create_direct_room,
isCreatingRoom = true
)
)
}
}
viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
renderCreateAndInviteState(it)
}
}
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,6 +129,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
} else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
}
} else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
finish()
}
}
}
@ -178,8 +198,12 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
}
companion object {
fun getIntent(context: Context): Intent {
return Intent(context, CreateDirectRoomActivity::class.java)
private const val BY_QR_CODE = "BY_QR_CODE"
fun getIntent(context: Context, byQrCode: Boolean = false): Intent {
return Intent(context, CreateDirectRoomActivity::class.java).apply {
putExtra(BY_QR_CODE, byQrCode)
}
}
}
}

View file

@ -0,0 +1,108 @@
/*
* 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.platform.VectorBaseFragment
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
override fun onResume() {
super.onResume()
// Register ourselves as a handler for scan results.
scannerView.setResultHandler(null)
// Start camera on resume
scannerView.startCamera()
}
override fun onPause() {
super.onPause()
// 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)
if (existingDm === null) {
// The following assumes MXIDs are case insensitive
if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) {
Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
// Try to get user from known users and fall back to creating a User object from MXID
val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
viewModel.handle(
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)))
)
}
} else {
navigator.openRoom(requireContext(), existingDm, null, false)
requireActivity().finish()
}
}
}
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

@ -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,8 @@ 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.DmsFabMenuView
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 +67,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, DmsFabMenuView.Listener, NotifsFabMenuView.Listener {
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@ -111,6 +111,7 @@ class RoomListFragment @Inject constructor(
}.exhaustive
}
createDmFabMenu.listener = this
createChatFabMenu.listener = this
sharedActionViewModel
@ -129,6 +130,7 @@ class RoomListFragment @Inject constructor(
roomListView.cleanup()
roomController.listener = null
stateRestorer.clear()
createDmFabMenu.listener = null
createChatFabMenu.listener = null
super.onDestroyView()
}
@ -140,33 +142,32 @@ class RoomListFragment @Inject constructor(
private fun setupCreateRoomButton() {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.isVisible = true
RoomListDisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
RoomListDisplayMode.PEOPLE -> createDmFabMenu.isVisible = true
RoomListDisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
else -> Unit // No button in this mode
}
createChatRoomButton.debouncedClicks {
createDirectChat()
}
createGroupRoomButton.debouncedClicks {
openRoomDirectory()
}
// Hide FAB when list is scrolling
// Hide FABs when list is scrolling
roomListView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
createDmFabMenu.removeCallbacks(showFabRunnable)
createChatFabMenu.removeCallbacks(showFabRunnable)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
createDmFabMenu.postDelayed(showFabRunnable, 250)
createChatFabMenu.postDelayed(showFabRunnable, 250)
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.hide()
RoomListDisplayMode.PEOPLE -> createChatRoomButton.hide()
RoomListDisplayMode.PEOPLE -> createDmFabMenu.hide()
RoomListDisplayMode.ROOMS -> createGroupRoomButton.hide()
else -> Unit
}
@ -191,6 +192,10 @@ class RoomListFragment @Inject constructor(
navigator.openCreateDirectRoom(requireActivity())
}
override fun createDirectChatByQrCode() {
navigator.openCreateDirectRoom(requireContext(), true)
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
@ -209,7 +214,7 @@ class RoomListFragment @Inject constructor(
if (isAdded) {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.show()
RoomListDisplayMode.PEOPLE -> createChatRoomButton.show()
RoomListDisplayMode.PEOPLE -> createDmFabMenu.show()
RoomListDisplayMode.ROOMS -> createGroupRoomButton.show()
else -> Unit
}
@ -338,6 +343,9 @@ class RoomListFragment @Inject constructor(
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
if (createDmFabMenu.onBackPressed()) {
return true
}
if (createChatFabMenu.onBackPressed()) {
return true
}

View file

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

View file

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

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

View file

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

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

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

@ -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,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="@color/riotx_header_panel_background_black" />
</FrameLayout>

View file

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

View file

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

View file

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

@ -1761,6 +1761,7 @@
<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="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>
@ -1828,6 +1829,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>
@ -2674,4 +2677,10 @@
<string name="warning_room_not_created_yet">The room is not yet created. Cancel the room creation?</string>
<string name="warning_unsaved_change">There are unsaved changes. Discard the changes?</string>
<string name="warning_unsaved_change_discard">Discard changes</string>
<!-- Add by QR code -->
<string name="share_by_text">Share by text</string>
<string name="cannot_dm_self">Cannot DM yourself!</string>
<string name="invalid_qr_code_uri">Invalid QR code (Invalid URI)!</string>
<string name="qr_code_not_scanned">QR code not scanned!</string>
</resources>

View file

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