Display Contact list (#548)
WIP (#548) WIP (#548) WIP (#548) WIP (#548) WIP (#548)
This commit is contained in:
parent
3842ec6bb0
commit
1c733e6661
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.core.contacts
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
/* TODO Rename to MxContact? */
|
||||||
|
|
||||||
|
class ContactModelBuilder(
|
||||||
|
val id: Long,
|
||||||
|
val displayName: String) {
|
||||||
|
|
||||||
|
var photoURI: Uri? = null
|
||||||
|
val msisdns = mutableListOf<MappedMsisdn>()
|
||||||
|
val emails = mutableListOf<MappedEmail>()
|
||||||
|
|
||||||
|
fun toContactModel(): ContactModel {
|
||||||
|
return ContactModel(
|
||||||
|
id = id,
|
||||||
|
displayName = displayName,
|
||||||
|
photoURI = photoURI,
|
||||||
|
msisdns = msisdns,
|
||||||
|
emails = emails
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ContactModel(
|
||||||
|
val id: Long,
|
||||||
|
val displayName: String,
|
||||||
|
val photoURI: Uri? = null,
|
||||||
|
val msisdns: List<MappedMsisdn> = emptyList(),
|
||||||
|
val emails: List<MappedEmail> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MappedEmail(
|
||||||
|
val email: String,
|
||||||
|
val matrixId: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MappedMsisdn(
|
||||||
|
val phoneNumber: String,
|
||||||
|
val matrixId: String?
|
||||||
|
)
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.core.contacts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ContactsDataSource @Inject constructor(
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun getContacts(): List<ContactModel> {
|
||||||
|
val result = mutableListOf<ContactModel>()
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
contentResolver.query(
|
||||||
|
ContactsContract.Contacts.CONTENT_URI,
|
||||||
|
null,
|
||||||
|
/* TODO
|
||||||
|
arrayOf(
|
||||||
|
ContactsContract.Contacts._ID,
|
||||||
|
ContactsContract.Data.DISPLAY_NAME,
|
||||||
|
ContactsContract.Data.PHOTO_URI,
|
||||||
|
ContactsContract.Data.MIMETYPE,
|
||||||
|
ContactsContract.CommonDataKinds.Phone.NUMBER,
|
||||||
|
ContactsContract.CommonDataKinds.Email.ADDRESS
|
||||||
|
),
|
||||||
|
*/
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
// Sort by Display name
|
||||||
|
ContactsContract.Data.DISPLAY_NAME
|
||||||
|
)
|
||||||
|
?.use { cursor ->
|
||||||
|
if (cursor.count > 0) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
|
||||||
|
val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
|
||||||
|
|
||||||
|
val currentContact = ContactModelBuilder(
|
||||||
|
id = id,
|
||||||
|
displayName = displayName
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.getString(ContactsContract.Data.PHOTO_URI)
|
||||||
|
?.let { Uri.parse(it) }
|
||||||
|
?.let { currentContact.photoURI = it }
|
||||||
|
|
||||||
|
// Get the phone numbers
|
||||||
|
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||||
|
null,
|
||||||
|
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
|
||||||
|
arrayOf(id.toString()),
|
||||||
|
null)
|
||||||
|
?.use { innerCursor ->
|
||||||
|
while (innerCursor.moveToNext()) {
|
||||||
|
innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||||
|
?.let {
|
||||||
|
currentContact.msisdns.add(
|
||||||
|
MappedMsisdn(
|
||||||
|
phoneNumber = it,
|
||||||
|
matrixId = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Emails
|
||||||
|
contentResolver.query(
|
||||||
|
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
||||||
|
null,
|
||||||
|
ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
|
||||||
|
arrayOf(id.toString()),
|
||||||
|
null)
|
||||||
|
?.use { innerCursor ->
|
||||||
|
while (innerCursor.moveToNext()) {
|
||||||
|
// This would allow you get several email addresses
|
||||||
|
// if the email addresses were stored in an array
|
||||||
|
innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
|
||||||
|
?.let {
|
||||||
|
currentContact.emails.add(
|
||||||
|
MappedEmail(
|
||||||
|
email = it,
|
||||||
|
matrixId = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(currentContact.toContactModel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
.filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getString(column: String): String? {
|
||||||
|
return getColumnIndex(column)
|
||||||
|
.takeIf { it != -1 }
|
||||||
|
?.let { getString(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getLong(column: String): Long? {
|
||||||
|
return getColumnIndex(column)
|
||||||
|
.takeIf { it != -1 }
|
||||||
|
?.let { getLong(it) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,6 +103,7 @@ import im.vector.riotx.features.share.IncomingShareFragment
|
||||||
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
|
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
|
||||||
import im.vector.riotx.features.terms.ReviewTermsFragment
|
import im.vector.riotx.features.terms.ReviewTermsFragment
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
||||||
|
import im.vector.riotx.features.userdirectory.PhoneBookFragment
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
||||||
import im.vector.riotx.features.widgets.WidgetFragment
|
import im.vector.riotx.features.widgets.WidgetFragment
|
||||||
|
|
||||||
|
@ -528,4 +529,9 @@ interface FragmentModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(WidgetFragment::class)
|
@FragmentKey(WidgetFragment::class)
|
||||||
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
|
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(PhoneBookFragment::class)
|
||||||
|
fun bindPhoneBookFragment(fragment: PhoneBookFragment): Fragment
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,12 @@ import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.extensions.addFragment
|
import im.vector.riotx.core.extensions.addFragment
|
||||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
||||||
|
import im.vector.riotx.features.userdirectory.PhoneBookViewModel
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
|
import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
||||||
|
@ -53,6 +55,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
||||||
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
||||||
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
|
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
|
||||||
|
@Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
|
||||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
@ -68,12 +71,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
.observe()
|
.observe()
|
||||||
.subscribe { sharedAction ->
|
.subscribe { sharedAction ->
|
||||||
when (sharedAction) {
|
when (sharedAction) {
|
||||||
UserDirectorySharedAction.OpenUsersDirectory ->
|
UserDirectorySharedAction.OpenUsersDirectory ->
|
||||||
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
|
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
|
||||||
UserDirectorySharedAction.Close -> finish()
|
UserDirectorySharedAction.Close -> finish()
|
||||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||||
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
||||||
}
|
UserDirectorySharedAction.OpenPhoneBook -> TODO()
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.util.MatrixItem
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.riotx.core.contacts.ContactModel
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.glide.GlideRequest
|
import im.vector.riotx.core.glide.GlideRequest
|
||||||
|
@ -63,6 +64,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
|
||||||
DrawableImageViewTarget(imageView))
|
DrawableImageViewTarget(imageView))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
fun render(contactModel: ContactModel, imageView: ImageView) {
|
||||||
|
// Create a Fake MatrixItem, for the placeholder
|
||||||
|
val matrixItem = MatrixItem.UserItem(
|
||||||
|
// Need an id starting with @
|
||||||
|
id = "@${contactModel.displayName}",
|
||||||
|
displayName = contactModel.displayName
|
||||||
|
)
|
||||||
|
|
||||||
|
val placeholder = getPlaceholderDrawable(imageView.context, matrixItem)
|
||||||
|
GlideApp.with(imageView)
|
||||||
|
.load(contactModel.photoURI)
|
||||||
|
.apply(RequestOptions.circleCropTransform())
|
||||||
|
.placeholder(placeholder)
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
fun render(context: Context,
|
fun render(context: Context,
|
||||||
glideRequests: GlideRequests,
|
glideRequests: GlideRequests,
|
||||||
|
|
|
@ -30,11 +30,14 @@ import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.extensions.addFragment
|
import im.vector.riotx.core.extensions.addFragment
|
||||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.utils.toast
|
import im.vector.riotx.core.utils.toast
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
||||||
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
|
||||||
|
import im.vector.riotx.features.userdirectory.PhoneBookFragment
|
||||||
|
import im.vector.riotx.features.userdirectory.PhoneBookViewModel
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
|
import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
|
||||||
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
||||||
|
@ -53,6 +56,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
|
||||||
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
||||||
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
||||||
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
|
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
|
||||||
|
@Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
|
||||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
@ -74,7 +78,9 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
|
||||||
UserDirectorySharedAction.Close -> finish()
|
UserDirectorySharedAction.Close -> finish()
|
||||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||||
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
||||||
}
|
UserDirectorySharedAction.OpenPhoneBook ->
|
||||||
|
addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
|
|
|
@ -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.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.ClickListener
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotx.core.epoxy.onClick
|
||||||
|
import im.vector.riotx.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.contacts.ContactModel
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_contact_main)
|
||||||
|
abstract class ContactItem : VectorEpoxyModel<ContactItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
@EpoxyAttribute lateinit var contact: ContactModel
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
// If name is empty, use userId as name and force it being centered
|
||||||
|
holder.nameView.text = contact.displayName
|
||||||
|
avatarRenderer.render(contact, holder.avatarImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val nameView by bind<TextView>(R.id.contactDisplayName)
|
||||||
|
val avatarImageView by bind<ImageView>(R.id.contactAvatar)
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,6 +63,7 @@ class KnownUsersFragment @Inject constructor(
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupFilterView()
|
setupFilterView()
|
||||||
setupAddByMatrixIdView()
|
setupAddByMatrixIdView()
|
||||||
|
setupAddFromPhoneBookView()
|
||||||
setupCloseView()
|
setupCloseView()
|
||||||
viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
|
viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
|
||||||
renderSelectedUsers(it)
|
renderSelectedUsers(it)
|
||||||
|
@ -96,6 +97,13 @@ class KnownUsersFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupAddFromPhoneBookView() {
|
||||||
|
addFromPhoneBook.debouncedClicks {
|
||||||
|
// TODO handle Permission first
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.OpenPhoneBook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
knownUsersController.callback = this
|
knownUsersController.callback = this
|
||||||
// Don't activate animation as we might have way to much item animation when filtering
|
// Don't activate animation as we might have way to much item animation when filtering
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class PhoneBookAction : VectorViewModelAction {
|
||||||
|
data class FilterWith(val filter: String) : PhoneBookAction()
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyController
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.contacts.ContactModel
|
||||||
|
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
||||||
|
import im.vector.riotx.core.epoxy.loadingItem
|
||||||
|
import im.vector.riotx.core.epoxy.noResultItem
|
||||||
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PhoneBookController @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val errorFormatter: ErrorFormatter) : EpoxyController() {
|
||||||
|
|
||||||
|
private var state: PhoneBookViewState? = null
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setData(state: PhoneBookViewState) {
|
||||||
|
this.state = state
|
||||||
|
requestModelBuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildModels() {
|
||||||
|
val currentState = state ?: return
|
||||||
|
val hasSearch = currentState.searchTerm.isNotBlank()
|
||||||
|
when (val asyncMappedContacts = currentState.mappedContacts) {
|
||||||
|
is Uninitialized -> renderEmptyState(false)
|
||||||
|
is Loading -> renderLoading()
|
||||||
|
is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch)
|
||||||
|
is Fail -> renderFailure(asyncMappedContacts.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderLoading() {
|
||||||
|
loadingItem {
|
||||||
|
id("loading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderFailure(failure: Throwable) {
|
||||||
|
errorWithRetryItem {
|
||||||
|
id("error")
|
||||||
|
text(errorFormatter.toHumanReadable(failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSuccess(mappedContacts: List<ContactModel>,
|
||||||
|
hasSearch: Boolean) {
|
||||||
|
if (mappedContacts.isEmpty()) {
|
||||||
|
renderEmptyState(hasSearch)
|
||||||
|
} else {
|
||||||
|
renderContacts(mappedContacts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderContacts(mappedContacts: List<ContactModel>) {
|
||||||
|
for (mappedContact in mappedContacts) {
|
||||||
|
contactItem {
|
||||||
|
id(mappedContact.id)
|
||||||
|
contact(mappedContact)
|
||||||
|
avatarRenderer(avatarRenderer)
|
||||||
|
}
|
||||||
|
mappedContact.emails.forEach {
|
||||||
|
contactDetailItem {
|
||||||
|
id("$mappedContact.id${it.email}")
|
||||||
|
threePid(it.email)
|
||||||
|
matrixId(it.matrixId)
|
||||||
|
clickListener {
|
||||||
|
if (it.matrixId != null) {
|
||||||
|
callback?.onMatrixIdClick(it.matrixId)
|
||||||
|
} else {
|
||||||
|
callback?.onThreePidClick(ThreePid.Email(it.email))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mappedContact.msisdns.forEach {
|
||||||
|
contactDetailItem {
|
||||||
|
id("$mappedContact.id${it.phoneNumber}")
|
||||||
|
threePid(it.phoneNumber)
|
||||||
|
matrixId(it.matrixId)
|
||||||
|
clickListener {
|
||||||
|
if (it.matrixId != null) {
|
||||||
|
callback?.onMatrixIdClick(it.matrixId)
|
||||||
|
} else {
|
||||||
|
callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderEmptyState(hasSearch: Boolean) {
|
||||||
|
val noResultRes = if (hasSearch) {
|
||||||
|
R.string.no_result_placeholder
|
||||||
|
} else {
|
||||||
|
R.string.empty_phone_book
|
||||||
|
}
|
||||||
|
noResultItem {
|
||||||
|
id("noResult")
|
||||||
|
text(stringProvider.getString(noResultRes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onMatrixIdClick(matrixId: String)
|
||||||
|
fun onThreePidClick(threePid: ThreePid)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.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.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.cleanup
|
||||||
|
import im.vector.riotx.core.extensions.configureWith
|
||||||
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_phonebook.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PhoneBookFragment @Inject constructor(
|
||||||
|
val phoneBookViewModelFactory: PhoneBookViewModel.Factory,
|
||||||
|
private val phoneBookController: PhoneBookController
|
||||||
|
) : VectorBaseFragment(), PhoneBookController.Callback {
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_phonebook
|
||||||
|
private val viewModel: UserDirectoryViewModel by activityViewModel()
|
||||||
|
|
||||||
|
// Use activityViewModel to avoid loading several times the data
|
||||||
|
private val phoneBookViewModel: PhoneBookViewModel 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()
|
||||||
|
setupFilterView()
|
||||||
|
setupCloseView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFilterView() {
|
||||||
|
phoneBookFilter
|
||||||
|
.textChanges()
|
||||||
|
.skipInitialValue()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.subscribe {
|
||||||
|
phoneBookViewModel.handle(PhoneBookAction.FilterWith(it.toString()))
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
phoneBookRecyclerView.cleanup()
|
||||||
|
phoneBookController.callback = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
phoneBookController.callback = this
|
||||||
|
phoneBookRecyclerView.configureWith(phoneBookController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupCloseView() {
|
||||||
|
phoneBookClose.debouncedClicks {
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(phoneBookViewModel) {
|
||||||
|
phoneBookController.setData(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMatrixIdClick(matrixId: String) {
|
||||||
|
view?.hideKeyboard()
|
||||||
|
viewModel.handle(UserDirectoryAction.SelectUser(User(matrixId)))
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onThreePidClick(threePid: ThreePid) {
|
||||||
|
view?.hideKeyboard()
|
||||||
|
viewModel.handle(UserDirectoryAction.SelectThreePid(threePid))
|
||||||
|
sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.airbnb.mvrx.ActivityViewModelContext
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.identity.FoundThreePid
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
|
import im.vector.riotx.core.contacts.ContactModel
|
||||||
|
import im.vector.riotx.core.contacts.ContactsDataSource
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
|
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||||
|
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private typealias PhoneBookSearch = String
|
||||||
|
|
||||||
|
class PhoneBookViewModel @AssistedInject constructor(@Assisted
|
||||||
|
initialState: PhoneBookViewState,
|
||||||
|
private val contactsDataSource: ContactsDataSource,
|
||||||
|
private val session: Session)
|
||||||
|
: VectorViewModel<PhoneBookViewState, PhoneBookAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: PhoneBookViewState): PhoneBookViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<PhoneBookViewModel, PhoneBookViewState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: PhoneBookViewState): PhoneBookViewModel? {
|
||||||
|
return when (viewModelContext) {
|
||||||
|
is FragmentViewModelContext -> (viewModelContext.fragment() as PhoneBookFragment).phoneBookViewModelFactory.create(state)
|
||||||
|
is ActivityViewModelContext -> {
|
||||||
|
when (viewModelContext.activity<FragmentActivity>()) {
|
||||||
|
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().phoneBookViewModelFactory.create(state)
|
||||||
|
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().phoneBookViewModelFactory.create(state)
|
||||||
|
else -> error("Wrong activity or fragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> error("Wrong activity or fragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var allContacts: List<ContactModel> = emptyList()
|
||||||
|
private var mappedContacts: List<ContactModel> = emptyList()
|
||||||
|
private var foundThreePid: List<FoundThreePid> = emptyList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadContacts()
|
||||||
|
|
||||||
|
selectSubscribe(PhoneBookViewState::searchTerm) {
|
||||||
|
updateState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadContacts() {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
mappedContacts = Loading()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
allContacts = contactsDataSource.getContacts()
|
||||||
|
mappedContacts = allContacts
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
mappedContacts = Success(allContacts)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
performLookup(allContacts)
|
||||||
|
updateState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performLookup(data: List<ContactModel>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val threePids = data.flatMap { contact ->
|
||||||
|
contact.emails.map { ThreePid.Email(it.email) } +
|
||||||
|
contact.msisdns.map { ThreePid.Msisdn(it.phoneNumber) }
|
||||||
|
}
|
||||||
|
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
// Ignore?
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: List<FoundThreePid>) {
|
||||||
|
foundThreePid = data
|
||||||
|
|
||||||
|
mappedContacts = allContacts.map { contactModel ->
|
||||||
|
contactModel.copy(
|
||||||
|
emails = contactModel.emails.map { email ->
|
||||||
|
email.copy(
|
||||||
|
matrixId = foundThreePid
|
||||||
|
.firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email }
|
||||||
|
?.matrixId
|
||||||
|
)
|
||||||
|
},
|
||||||
|
msisdns = contactModel.msisdns.map { msisdn ->
|
||||||
|
msisdn.copy(
|
||||||
|
matrixId = foundThreePid
|
||||||
|
.firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber }
|
||||||
|
?.matrixId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState() = withState { state ->
|
||||||
|
val filteredMappedContacts = mappedContacts
|
||||||
|
.filter { it.displayName.contains(state.searchTerm, true) }
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
filteredMappedContacts = filteredMappedContacts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: PhoneBookAction) {
|
||||||
|
when (action) {
|
||||||
|
is PhoneBookAction.FilterWith -> handleFilterWith(action)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFilterWith(action: PhoneBookAction.FilterWith) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
searchTerm = action.filter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import im.vector.riotx.core.contacts.ContactModel
|
||||||
|
|
||||||
|
data class PhoneBookViewState(
|
||||||
|
val searchTerm: String = "",
|
||||||
|
val mappedContacts: Async<List<ContactModel>> = Loading(),
|
||||||
|
val filteredMappedContacts: List<ContactModel> = emptyList()
|
||||||
|
/*
|
||||||
|
val knownUsers: Async<PagedList<User>> = Uninitialized,
|
||||||
|
val directoryUsers: Async<List<User>> = Uninitialized,
|
||||||
|
val selectedUsers: Set<User> = emptySet(),
|
||||||
|
val createAndInviteState: Async<String> = Uninitialized,
|
||||||
|
val filterKnownUsersValue: Option<String> = Option.empty()
|
||||||
|
*/
|
||||||
|
) : MvRxState
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.userdirectory
|
package im.vector.riotx.features.userdirectory
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
@ -24,5 +25,6 @@ sealed class UserDirectoryAction : VectorViewModelAction {
|
||||||
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
|
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
|
||||||
object ClearFilterKnownUsers : UserDirectoryAction()
|
object ClearFilterKnownUsers : UserDirectoryAction()
|
||||||
data class SelectUser(val user: User) : UserDirectoryAction()
|
data class SelectUser(val user: User) : UserDirectoryAction()
|
||||||
|
data class SelectThreePid(val threePid: ThreePid) : UserDirectoryAction()
|
||||||
data class RemoveSelectedUser(val user: User) : UserDirectoryAction()
|
data class RemoveSelectedUser(val user: User) : UserDirectoryAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.riotx.core.platform.VectorSharedAction
|
||||||
|
|
||||||
sealed class UserDirectorySharedAction : VectorSharedAction {
|
sealed class UserDirectorySharedAction : VectorSharedAction {
|
||||||
object OpenUsersDirectory : UserDirectorySharedAction()
|
object OpenUsersDirectory : UserDirectorySharedAction()
|
||||||
|
object OpenPhoneBook : UserDirectorySharedAction()
|
||||||
object Close : UserDirectorySharedAction()
|
object Close : UserDirectorySharedAction()
|
||||||
object GoBack : UserDirectorySharedAction()
|
object GoBack : UserDirectorySharedAction()
|
||||||
data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set<User>) : UserDirectorySharedAction()
|
data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set<User>) : UserDirectorySharedAction()
|
||||||
|
|
|
@ -123,6 +123,23 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/knownUsersFilterDivider" />
|
app:layout_constraintTop_toBottomOf="@id/knownUsersFilterDivider" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/addFromPhoneBook"
|
||||||
|
style="@style/VectorButtonStyleText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:minHeight="@dimen/layout_touch_size"
|
||||||
|
android:text="@string/add_from_phone_book"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:icon="@drawable/ic_plus_circle"
|
||||||
|
app:iconPadding="13dp"
|
||||||
|
app:iconTint="@color/riotx_accent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/addByMatrixId" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -134,7 +151,7 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/addByMatrixId"
|
app:layout_constraintTop_toBottomOf="@+id/addFromPhoneBook"
|
||||||
tools:listitem="@layout/item_known_user" />
|
tools:listitem="@layout/item_known_user" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/phoneBookToolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
android:elevation="4dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/phoneBookClose"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_x_18dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<im.vector.riotx.core.platform.EllipsizingTextView
|
||||||
|
android:id="@+id/phoneBookTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/phone_book_title"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/phoneBookClose"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/phoneBookFilterContainer"
|
||||||
|
style="@style/VectorTextInputLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookToolbar">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/phoneBookFilter"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/search" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/phoneBookFilterDivider"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="?attr/vctr_list_divider_color"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/phoneBookRecyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:fastScrollEnabled="true"
|
||||||
|
android:overScrollMode="always"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterDivider"
|
||||||
|
tools:listitem="@layout/item_contact_main" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:minHeight="60dp"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<im.vector.riotx.core.platform.EllipsizingTextView
|
||||||
|
android:id="@+id/contactDetailName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/contactDetailMatrixId"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
<im.vector.riotx.core.platform.EllipsizingTextView
|
||||||
|
android:id="@+id/contactDetailMatrixId"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/contactDetailName"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/contactDetailName"
|
||||||
|
tools:text="@sample/matrix.json/data/mxid"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:minHeight="72dp"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/contactAvatar"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<im.vector.riotx.core.platform.EllipsizingTextView
|
||||||
|
android:id="@+id/contactDisplayName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/contactAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -2541,4 +2541,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||||
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
|
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
|
||||||
|
|
||||||
<string name="save_recovery_key_chooser_hint">Save recovery key in</string>
|
<string name="save_recovery_key_chooser_hint">Save recovery key in</string>
|
||||||
|
|
||||||
|
<string name="add_from_phone_book">Add from my phone book</string>
|
||||||
|
<string name="empty_phone_book">Your phone book is empty</string>
|
||||||
|
<string name="phone_book_title">Phone book</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue