Skip to content

Commit e8d084b

Browse files
TR-SLimeyBillCarsonFr
authored andcommitted
Add ability to share profile by QR code
1 parent 5b278f7 commit e8d084b

22 files changed

+680
-71
lines changed

AUTHORS.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ We do not forget all translators, for their work of translating Element into man
3939

4040
Feel free to add your name below, when you contribute to the project!
4141

42-
Name | Matrix ID | GitHub
43-
--------|---------------------|--------------------------------------
44-
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
45-
42+
Name | Matrix ID | GitHub
43+
----------|-----------------------------|--------------------------------------
44+
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
45+
TR_SLimey | @tr_slimey:an-atom-in.space | [TR-SLimey](https://github.com/TR-SLimey)

CHANGES.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Changes in Element 1.0.11 (2020-XX-XX)
22
===================================================
33

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

77
Improvements 🙌:
88
- New room creation tile with quick action (#2346)

vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt

+49-25
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.content.Context
2222
import android.content.Intent
2323
import android.os.Bundle
2424
import android.view.View
25+
import android.widget.Toast
2526
import androidx.appcompat.app.AlertDialog
2627
import com.airbnb.mvrx.Async
2728
import com.airbnb.mvrx.Fail
@@ -37,6 +38,8 @@ import im.vector.app.core.extensions.exhaustive
3738
import im.vector.app.core.platform.SimpleFragmentActivity
3839
import im.vector.app.core.platform.WaitingViewData
3940
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
41+
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
42+
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
4043
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
4144
import im.vector.app.core.utils.allGranted
4245
import im.vector.app.core.utils.checkPermissions
@@ -72,35 +75,45 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
7275
super.onCreate(savedInstanceState)
7376
toolbar.visibility = View.GONE
7477
sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
75-
sharedActionViewModel
76-
.observe()
77-
.subscribe { sharedAction ->
78-
when (sharedAction) {
79-
UserDirectorySharedAction.OpenUsersDirectory ->
80-
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
81-
UserDirectorySharedAction.Close -> finish()
82-
UserDirectorySharedAction.GoBack -> onBackPressed()
83-
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
84-
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
85-
}.exhaustive
86-
}
87-
.disposeOnDestroy()
88-
if (isFirstCreation()) {
89-
addFragment(
90-
R.id.container,
91-
KnownUsersFragment::class.java,
92-
KnownUsersFragmentArgs(
93-
title = getString(R.string.fab_menu_create_chat),
94-
menuResId = R.menu.vector_create_direct_room,
95-
isCreatingRoom = true
96-
)
97-
)
78+
if (intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
79+
if (isFirstCreation()) { openAddByQrCode() }
80+
} else {
81+
sharedActionViewModel
82+
.observe()
83+
.subscribe { sharedAction ->
84+
when (sharedAction) {
85+
UserDirectorySharedAction.OpenUsersDirectory ->
86+
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
87+
UserDirectorySharedAction.Close -> finish()
88+
UserDirectorySharedAction.GoBack -> onBackPressed()
89+
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
90+
UserDirectorySharedAction.OpenPhoneBook -> openPhoneBook()
91+
}.exhaustive
92+
}
93+
.disposeOnDestroy()
94+
if (isFirstCreation()) {
95+
addFragment(
96+
R.id.container,
97+
KnownUsersFragment::class.java,
98+
KnownUsersFragmentArgs(
99+
title = getString(R.string.fab_menu_create_chat),
100+
menuResId = R.menu.vector_create_direct_room,
101+
isCreatingRoom = true
102+
)
103+
)
104+
}
98105
}
99106
viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
100107
renderCreateAndInviteState(it)
101108
}
102109
}
103110

111+
private fun openAddByQrCode() {
112+
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA, 0)) {
113+
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
114+
}
115+
}
116+
104117
private fun openPhoneBook() {
105118
// Check permission first
106119
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
@@ -116,6 +129,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
116129
if (allGranted(grantResults)) {
117130
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
118131
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
132+
} else if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
133+
addFragment(R.id.container, CreateDirectRoomByQrCodeFragment::class.java)
134+
}
135+
} else {
136+
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
137+
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && intent?.getBooleanExtra(BY_QR_CODE, false)!!) {
138+
finish()
119139
}
120140
}
121141
}
@@ -178,8 +198,12 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
178198
}
179199

180200
companion object {
181-
fun getIntent(context: Context): Intent {
182-
return Intent(context, CreateDirectRoomActivity::class.java)
201+
private const val BY_QR_CODE = "BY_QR_CODE"
202+
203+
fun getIntent(context: Context, byQrCode: Boolean = false): Intent {
204+
return Intent(context, CreateDirectRoomActivity::class.java).apply {
205+
putExtra(BY_QR_CODE, byQrCode)
206+
}
183207
}
184208
}
185209
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2020 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.createdirect
18+
19+
import android.widget.Toast
20+
import com.airbnb.mvrx.activityViewModel
21+
import com.google.zxing.Result
22+
import com.google.zxing.ResultMetadataType
23+
import im.vector.app.R
24+
import im.vector.app.core.platform.VectorBaseFragment
25+
import im.vector.app.features.userdirectory.PendingInvitee
26+
import kotlinx.android.synthetic.main.fragment_qr_code_scanner.*
27+
import me.dm7.barcodescanner.zxing.ZXingScannerView
28+
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
29+
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
30+
import org.matrix.android.sdk.api.session.user.model.User
31+
import javax.inject.Inject
32+
33+
class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragment(), ZXingScannerView.ResultHandler {
34+
35+
private val viewModel: CreateDirectRoomViewModel by activityViewModel()
36+
37+
override fun getLayoutResId() = R.layout.fragment_qr_code_scanner
38+
39+
override fun onResume() {
40+
super.onResume()
41+
// Register ourselves as a handler for scan results.
42+
scannerView.setResultHandler(null)
43+
// Start camera on resume
44+
scannerView.startCamera()
45+
}
46+
47+
override fun onPause() {
48+
super.onPause()
49+
// Stop camera on pause
50+
scannerView.stopCamera()
51+
}
52+
53+
// Copied from https://github.com/markusfisch/BinaryEye/blob/
54+
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
55+
private fun getRawBytes(result: Result): ByteArray? {
56+
val metadata = result.resultMetadata ?: return null
57+
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
58+
var bytes = ByteArray(0)
59+
@Suppress("UNCHECKED_CAST")
60+
for (seg in segments as Iterable<ByteArray>) {
61+
bytes += seg
62+
}
63+
// byte segments can never be shorter than the text.
64+
// Zxing cuts off content prefixes like "WIFI:"
65+
return if (bytes.size >= result.text.length) bytes else null
66+
}
67+
68+
private fun addByQrCode(value: String) {
69+
val mxid = (PermalinkParser.parse(value) as? PermalinkData.UserLink)?.userId
70+
71+
if (mxid === null) {
72+
Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show()
73+
requireActivity().finish()
74+
} else {
75+
val existingDm = viewModel.session.getExistingDirectRoomWithUser(mxid)
76+
77+
if (existingDm === null) {
78+
// The following assumes MXIDs are case insensitive
79+
if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) {
80+
Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
81+
requireActivity().finish()
82+
} else {
83+
// Try to get user from known users and fall back to creating a User object from MXID
84+
val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
85+
86+
viewModel.handle(
87+
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)))
88+
)
89+
}
90+
} else {
91+
navigator.openRoom(requireContext(), existingDm, null, false)
92+
requireActivity().finish()
93+
}
94+
}
95+
}
96+
97+
override fun handleResult(result: Result?) {
98+
if (result === null) {
99+
Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
100+
requireActivity().finish()
101+
} else {
102+
val rawBytes = getRawBytes(result)
103+
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
104+
val value = rawBytesStr ?: result.text
105+
addByQrCode(value)
106+
}
107+
}
108+
}

vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import org.matrix.android.sdk.rx.rx
3838
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
3939
initialState: CreateDirectRoomViewState,
4040
private val rawService: RawService,
41-
private val session: Session)
41+
val session: Session)
4242
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
4343

4444
@AssistedInject.Factory

vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import com.airbnb.epoxy.EpoxyModelClass
2222
import im.vector.app.R
2323
import im.vector.app.core.epoxy.VectorEpoxyHolder
2424
import im.vector.app.core.epoxy.VectorEpoxyModel
25-
import im.vector.app.features.home.room.list.widget.FabMenuView
25+
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
2626

2727
@EpoxyModelClass(layout = R.layout.item_room_filter_footer)
2828
abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.Holder>() {
@@ -46,7 +46,7 @@ abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.
4646
val openRoomDirectory by bind<Button>(R.id.roomFilterFooterOpenRoomDirectory)
4747
}
4848

49-
interface FilteredRoomFooterItemListener : FabMenuView.Listener {
49+
interface FilteredRoomFooterItemListener : NotifsFabMenuView.Listener {
5050
fun createRoom(initialName: String)
5151
}
5252
}

vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt

+18-10
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ import im.vector.app.features.home.room.list.actions.RoomListActionsArgs
4545
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
4646
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
4747
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
48-
import im.vector.app.features.home.room.list.widget.FabMenuView
48+
import im.vector.app.features.home.room.list.widget.DmsFabMenuView
49+
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
4950
import im.vector.app.features.notifications.NotificationDrawerManager
5051
import kotlinx.android.parcel.Parcelize
5152
import kotlinx.android.synthetic.main.fragment_room_list.*
@@ -66,8 +67,7 @@ class RoomListFragment @Inject constructor(
6667
val roomListViewModelFactory: RoomListViewModel.Factory,
6768
private val notificationDrawerManager: NotificationDrawerManager,
6869
private val sharedViewPool: RecyclerView.RecycledViewPool
69-
70-
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
70+
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, DmsFabMenuView.Listener, NotifsFabMenuView.Listener {
7171

7272
private var modelBuildListener: OnModelBuildFinishedListener? = null
7373
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@@ -111,6 +111,7 @@ class RoomListFragment @Inject constructor(
111111
}.exhaustive
112112
}
113113

114+
createDmFabMenu.listener = this
114115
createChatFabMenu.listener = this
115116

116117
sharedActionViewModel
@@ -129,6 +130,7 @@ class RoomListFragment @Inject constructor(
129130
roomListView.cleanup()
130131
roomController.listener = null
131132
stateRestorer.clear()
133+
createDmFabMenu.listener = null
132134
createChatFabMenu.listener = null
133135
super.onDestroyView()
134136
}
@@ -140,33 +142,32 @@ class RoomListFragment @Inject constructor(
140142
private fun setupCreateRoomButton() {
141143
when (roomListParams.displayMode) {
142144
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.isVisible = true
143-
RoomListDisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
145+
RoomListDisplayMode.PEOPLE -> createDmFabMenu.isVisible = true
144146
RoomListDisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
145147
else -> Unit // No button in this mode
146148
}
147149

148-
createChatRoomButton.debouncedClicks {
149-
createDirectChat()
150-
}
151150
createGroupRoomButton.debouncedClicks {
152151
openRoomDirectory()
153152
}
154153

155-
// Hide FAB when list is scrolling
154+
// Hide FABs when list is scrolling
156155
roomListView.addOnScrollListener(
157156
object : RecyclerView.OnScrollListener() {
158157
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
158+
createDmFabMenu.removeCallbacks(showFabRunnable)
159159
createChatFabMenu.removeCallbacks(showFabRunnable)
160160

161161
when (newState) {
162162
RecyclerView.SCROLL_STATE_IDLE -> {
163+
createDmFabMenu.postDelayed(showFabRunnable, 250)
163164
createChatFabMenu.postDelayed(showFabRunnable, 250)
164165
}
165166
RecyclerView.SCROLL_STATE_DRAGGING,
166167
RecyclerView.SCROLL_STATE_SETTLING -> {
167168
when (roomListParams.displayMode) {
168169
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.hide()
169-
RoomListDisplayMode.PEOPLE -> createChatRoomButton.hide()
170+
RoomListDisplayMode.PEOPLE -> createDmFabMenu.hide()
170171
RoomListDisplayMode.ROOMS -> createGroupRoomButton.hide()
171172
else -> Unit
172173
}
@@ -191,6 +192,10 @@ class RoomListFragment @Inject constructor(
191192
navigator.openCreateDirectRoom(requireActivity())
192193
}
193194

195+
override fun createDirectChatByQrCode() {
196+
navigator.openCreateDirectRoom(requireContext(), true)
197+
}
198+
194199
private fun setupRecyclerView() {
195200
val layoutManager = LinearLayoutManager(context)
196201
stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
@@ -209,7 +214,7 @@ class RoomListFragment @Inject constructor(
209214
if (isAdded) {
210215
when (roomListParams.displayMode) {
211216
RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.show()
212-
RoomListDisplayMode.PEOPLE -> createChatRoomButton.show()
217+
RoomListDisplayMode.PEOPLE -> createDmFabMenu.show()
213218
RoomListDisplayMode.ROOMS -> createGroupRoomButton.show()
214219
else -> Unit
215220
}
@@ -338,6 +343,9 @@ class RoomListFragment @Inject constructor(
338343
}
339344

340345
override fun onBackPressed(toolbarButton: Boolean): Boolean {
346+
if (createDmFabMenu.onBackPressed()) {
347+
return true
348+
}
341349
if (createChatFabMenu.onBackPressed()) {
342350
return true
343351
}

0 commit comments

Comments
 (0)