Skip to content

Add users by QR code #2289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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)
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Changes in Element 1.0.10 (2020-XX-XX)
===================================================

Features ✨:
-
- Create DMs with users by scanning their QR code (#2025)

Improvements 🙌:
- Rework sending Event management (#154)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -72,34 +75,44 @@ 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
)
)
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
)
)
}
}
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,
Expand All @@ -115,6 +128,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()
}
}
}
Expand Down Expand Up @@ -174,8 +194,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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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 {
// This and the related conditional can be removed when PR #2342 is merged
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>() {
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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
Expand Down Expand Up @@ -111,6 +111,7 @@ class RoomListFragment @Inject constructor(
}.exhaustive
}

createDmFabMenu.listener = this
createChatFabMenu.listener = this

sharedActionViewModel
Expand All @@ -129,6 +130,7 @@ class RoomListFragment @Inject constructor(
roomListView.cleanup()
roomController.listener = null
stateRestorer.clear()
createDmFabMenu.listener = null
createChatFabMenu.listener = null
super.onDestroyView()
}
Expand All @@ -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
}
Expand All @@ -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()
Expand All @@ -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
}
Expand Down Expand Up @@ -338,6 +343,9 @@ class RoomListFragment @Inject constructor(
}

override fun onBackPressed(toolbarButton: Boolean): Boolean {
if (createDmFabMenu.onBackPressed()) {
return true
}
if (createChatFabMenu.onBackPressed()) {
return true
}
Expand Down
Loading