M AUTHORS.md => AUTHORS.md +4 -4
@@ 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)
M CHANGES.md => CHANGES.md +4 -1
@@ 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)
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +1 -1
@@ 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,
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +1 -1
@@ 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() }
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +1 -1
@@ 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()
}
}
}
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt +1 -1
@@ 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(
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/typing/DefaultTypingUsersTracker.kt +1 -1
@@ 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()
}
}
M matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt => matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +1 -1
@@ 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()
}
}
M vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt => vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +4 -1
@@ 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()
M => +1 -1
@@ 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>
M vector/src/main/AndroidManifest.xml => vector/src/main/AndroidManifest.xml +1 -0
@@ 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 -->
M vector/src/main/java/im/vector/app/core/di/FragmentModule.kt => vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +9 -9
@@ 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
}
M vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt => vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +4 -0
@@ 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
M vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt => vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +3 -3
@@ 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
A vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt => vector/src/main/java/im/vector/app/core/epoxy/CheckBoxItem.kt +46 -0
@@ 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)
+ }
+}
M vector/src/main/java/im/vector/app/core/extensions/EditText.kt => vector/src/main/java/im/vector/app/core/extensions/EditText.kt +1 -1
@@ 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) {
M vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt => vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +10 -0
@@ 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
* ========================================================================================== */
M vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt => vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +14 -0
@@ 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
*/
M vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt => vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +7 -0
@@ 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).
M vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt => vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +7 -1
@@ 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) {
M vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt => vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +12 -12
@@ 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)
}
}
M vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt => vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +45 -24
@@ 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)
}
A vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt => vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +122 -0
@@ 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)
+ }
+ }
+}
M vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt => vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +1 -1
@@ 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
M vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt => vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +30 -0
@@ 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 {
M vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt => vector/src/main/java/im/vector/app/features/home/HomeSharedActionViewModel.kt +2 -1
@@ 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>()
M vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt => vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomFooterItem.kt +2 -2
@@ 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)
}
}
M vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt => vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +2 -3
@@ 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
R vector/src/main/java/im/vector/app/features/home/room/list/widget/FabMenuView.kt => vector/src/main/java/im/vector/app/features/home/room/list/widget/NotifsFabMenuView.kt +4 -4
@@ 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() {
M vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt => vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt +2 -2
@@ 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)
}
M vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt => vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +37 -27
@@ 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()
}
}
A vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt => vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheet.kt +65 -0
@@ 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
+ }
+ }
+ }
+}
M vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt => vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +3 -3
@@ 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.
M vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt => vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt +2 -2
@@ 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)
}
}
M vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt => vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +11 -0
@@ 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) {
M vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt => vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +14 -6
@@ 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) {
M vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt => vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +2 -15
@@ 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
}
A vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt => vector/src/main/java/im/vector/app/features/usercode/QRCodeBitmapDecodeHelper.kt +85 -0
@@ 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()
+ }
+ }
+}
A vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt => vector/src/main/java/im/vector/app/features/usercode/ScanUserCodeFragment.kt +148 -0
@@ 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
+ }
+}
A vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt => vector/src/main/java/im/vector/app/features/usercode/ShowUserCodeFragment.kt +87 -0
@@ 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)
+ }
+}
A vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeActions.kt +29 -0
@@ 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()
+}
A vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +129 -0
@@ 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))
+ }
+ }
+ }
+}
A vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeShareViewEvents.kt +29 -0
@@ 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()
+}
A vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +162 -0
@@ 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)
+ }
+ }
+}
A vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt => vector/src/main/java/im/vector/app/features/usercode/UserCodeState.kt +37 -0
@@ 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
+ )
+}
A vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/ActionItem.kt +54 -0
@@ 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)
+ }
+}
A vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/ContactDetailItem.kt +47 -0
@@ 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)
+ }
+}
A vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/ContactItem.kt +46 -0
@@ 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)
+ }
+}
D vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt => vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersController.kt +0 -122
@@ 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)
- }
-}
D vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryFragment.kt +0 -94
@@ 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))
- }
-}
R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryAction.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt +6 -6
@@ 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()
}
R vector/src/main/java/im/vector/app/features/userdirectory/DirectoryUsersController.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt +124 -66
@@ 16,6 16,7 @@
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
@@ 28,112 29,169 @@ 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.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 DirectoryUsersController @Inject constructor(private val session: Session,
- private val avatarRenderer: AvatarRenderer,
- private val stringProvider: StringProvider,
- private val errorFormatter: ErrorFormatter) : EpoxyController() {
+class UserListController @Inject constructor(private val session: Session,
+ private val avatarRenderer: AvatarRenderer,
+ private val stringProvider: StringProvider,
+ private val errorFormatter: ErrorFormatter) : EpoxyController() {
- private var state: UserDirectoryViewState? = null
+ private var state: UserListViewState? = null
var callback: Callback? = null
- init {
- requestModelBuild()
- }
-
- fun setData(state: UserDirectoryViewState) {
+ fun setData(state: UserListViewState) {
this.state = state
requestModelBuild()
}
override fun buildModels() {
val currentState = state ?: return
- val hasSearch = currentState.directorySearchTerm.isNotBlank()
+
+ // 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 -> renderEmptyState(false)
+ is Uninitialized -> {
+ }
is Loading -> renderLoading()
- is Success -> renderSuccess(
- computeUsersList(asyncUsers(), currentState.directorySearchTerm),
+ is Fail -> renderFailure(asyncUsers.error)
+ is Success -> buildDirectoryUsers(
+ asyncUsers(),
currentState.getSelectedMatrixId(),
- hasSearch
+ currentState.searchTerm,
+ // to avoid showing twice same user in known and suggestions
+ currentState.knownUsers.invoke()?.map { it.userId }.orEmpty()
)
- 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 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))
+ }
- private fun renderLoading() {
- loadingItem {
- id("loading")
+ 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 renderFailure(failure: Throwable) {
- errorWithRetryItem {
- id("error")
- text(errorFormatter.toHumanReadable(failure))
- listener { callback?.retryDirectoryUsersRequest() }
+ 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
}
- }
-
- private fun renderSuccess(users: List<User>,
- selectedUsers: List<String>,
- hasSearch: Boolean) {
- if (users.isEmpty()) {
- renderEmptyState(hasSearch)
- } else {
- renderUsers(users, selectedUsers)
+ userListHeaderItem {
+ id("suggestions")
+ header(stringProvider.getString(R.string.direct_room_user_list_suggestions_title))
}
- }
-
- 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)
+ 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 renderEmptyState(hasSearch: Boolean) {
- val noResultRes = if (hasSearch) {
- R.string.no_result_placeholder
- } else {
- R.string.direct_room_start_search
+ private fun renderLoading() {
+ loadingItem {
+ id("loading")
}
+ }
+
+ private fun renderEmptyState() {
noResultItem {
id("noResult")
- text(stringProvider.getString(noResultRes))
+ 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 retryDirectoryUsersRequest()
+ fun onMatrixIdClick(matrixId: String)
+ fun onThreePidClick(threePid: ThreePid)
}
}
R vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragment.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +75 -58
@@ 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)
}
}
R vector/src/main/java/im/vector/app/features/userdirectory/KnownUsersFragmentArgs.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt +2 -2
@@ 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
A vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListHeaderItem.kt +39 -0
@@ 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)
+ }
+}
R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedAction.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt +6 -8
@@ 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()
}
R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectorySharedActionViewModel.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedActionViewModel.kt +1 -1
@@ 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>()
R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewEvents.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt +3 -1
@@ 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()
+}
R vector/src/main/java/im/vector/app/features/userdirectory/UserDirectoryViewModel.kt => vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +97 -70
@@ 16,8 16,6 @@
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
@@ 28,126 26,155 @@ 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 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 KnowUsersFilter = String
+private typealias KnownUsersSearch = String
private typealias DirectoryUsersSearch = String
-class UserDirectoryViewModel @AssistedInject constructor(@Assisted
- initialState: UserDirectoryViewState,
- private val session: Session)
- : VectorViewModel<UserDirectoryViewState, UserDirectoryAction, UserDirectoryViewEvents>(initialState) {
+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: UserDirectoryViewState): UserDirectoryViewModel
+ fun create(initialState: UserListViewState): UserListViewModel
}
- private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
- private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
-
- companion object : MvRxViewModelFactory<UserDirectoryViewModel, UserDirectoryViewState> {
+ companion object : MvRxViewModelFactory<UserListViewModel, UserListViewState> {
- 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 -> vi