mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Display Contact list (#548)
WIP (#548) WIP (#548) WIP (#548) WIP (#548) WIP (#548)
This commit is contained in:
parent
3842ec6bb0
commit
1c733e6661
21 changed files with 1013 additions and 6 deletions
|
@ -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.terms.ReviewTermsFragment
|
||||
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.widgets.WidgetFragment
|
||||
|
||||
|
@ -528,4 +529,9 @@ interface FragmentModule {
|
|||
@IntoMap
|
||||
@FragmentKey(WidgetFragment::class)
|
||||
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.extensions.addFragment
|
||||
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.WaitingViewData
|
||||
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
||||
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.UserDirectorySharedAction
|
||||
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
||||
|
@ -53,6 +55,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
|||
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
||||
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
||||
@Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
|
||||
@Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
|
@ -68,12 +71,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
|||
.observe()
|
||||
.subscribe { sharedAction ->
|
||||
when (sharedAction) {
|
||||
UserDirectorySharedAction.OpenUsersDirectory ->
|
||||
UserDirectorySharedAction.OpenUsersDirectory ->
|
||||
addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
|
||||
UserDirectorySharedAction.Close -> finish()
|
||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||
UserDirectorySharedAction.Close -> finish()
|
||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
||||
}
|
||||
UserDirectorySharedAction.OpenPhoneBook -> TODO()
|
||||
}.exhaustive
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
if (isFirstCreation()) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
|
|||
import com.bumptech.glide.request.target.Target
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
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.glide.GlideApp
|
||||
import im.vector.riotx.core.glide.GlideRequest
|
||||
|
@ -63,6 +64,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
|
|||
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
|
||||
fun render(context: Context,
|
||||
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.extensions.addFragment
|
||||
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.WaitingViewData
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import im.vector.riotx.features.userdirectory.KnownUsersFragment
|
||||
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.UserDirectorySharedAction
|
||||
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
|
||||
|
@ -53,6 +56,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
|
|||
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
|
||||
@Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
|
||||
@Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
|
||||
@Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
|
@ -74,7 +78,9 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
|
|||
UserDirectorySharedAction.Close -> finish()
|
||||
UserDirectorySharedAction.GoBack -> onBackPressed()
|
||||
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
|
||||
}
|
||||
UserDirectorySharedAction.OpenPhoneBook ->
|
||||
addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
|
||||
}.exhaustive
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
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()
|
||||
setupFilterView()
|
||||
setupAddByMatrixIdView()
|
||||
setupAddFromPhoneBookView()
|
||||
setupCloseView()
|
||||
viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
|
||||
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() {
|
||||
knownUsersController.callback = this
|
||||
// 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
|
||||
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
|
||||
|
@ -24,5 +25,6 @@ sealed class UserDirectoryAction : VectorViewModelAction {
|
|||
data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
|
||||
object ClearFilterKnownUsers : UserDirectoryAction()
|
||||
data class SelectUser(val user: User) : UserDirectoryAction()
|
||||
data class SelectThreePid(val threePid: ThreePid) : UserDirectoryAction()
|
||||
data class RemoveSelectedUser(val user: User) : UserDirectoryAction()
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.riotx.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 selectedUsers: Set<User>) : UserDirectorySharedAction()
|
||||
|
|
|
@ -123,6 +123,23 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
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
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
|
@ -134,7 +151,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/addByMatrixId"
|
||||
app:layout_constraintTop_toBottomOf="@+id/addFromPhoneBook"
|
||||
tools:listitem="@layout/item_known_user" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
109
vector/src/main/res/layout/fragment_phonebook.xml
Normal file
109
vector/src/main/res/layout/fragment_phonebook.xml
Normal file
|
@ -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>
|
||||
|
46
vector/src/main/res/layout/item_contact_detail.xml
Normal file
46
vector/src/main/res/layout/item_contact_detail.xml
Normal file
|
@ -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>
|
38
vector/src/main/res/layout/item_contact_main.xml
Normal file
38
vector/src/main/res/layout/item_contact_main.xml
Normal file
|
@ -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="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>
|
||||
|
|
Loading…
Reference in a new issue