diff --git a/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersDialogFragment.kt b/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersDialogFragment.kt index ecbb830d94cb106b5142b2d8544da016a98c944c..8812ae6f4930e9e5c988692fa7f06a1e4a9e9925 100644 --- a/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersDialogFragment.kt +++ b/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersDialogFragment.kt @@ -9,7 +9,6 @@ import com.futo.circles.databinding.InviteMembersDialogFragmentBinding import com.futo.circles.extensions.getQueryTextChangeStateFlow import com.futo.circles.extensions.observeData import com.futo.circles.feature.group_invite.list.InviteMembersListAdapter -import com.futo.circles.model.RoomMemberListItem import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -20,7 +19,7 @@ class InviteMembersDialogFragment : private val args: InviteMembersDialogFragmentArgs by navArgs() private val viewModel by viewModel<InviteMembersViewModel> { parametersOf(args.roomId) } - private val listAdapter by lazy { InviteMembersListAdapter() } + private val listAdapter by lazy { InviteMembersListAdapter(viewModel::onUserSelected) } private val binding by lazy { getBinding() as InviteMembersDialogFragmentBinding @@ -45,13 +44,8 @@ class InviteMembersDialogFragment : binding.toolbar.title = it } - viewModel.usersLiveData.observeData(this) { users -> - setUserList(users) + viewModel.usersLiveData.observeData(this) { items -> + listAdapter.submitList(items) } } - - private fun setUserList(users: List<RoomMemberListItem>) { - listAdapter.submitList(users) - } - } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersViewModel.kt b/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersViewModel.kt index 380096d8c8709ed691f95decc2903c51d453bbb1..edc85c1f849d8f076ea21c1a7ea885697e427cfa 100644 --- a/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersViewModel.kt +++ b/app/src/main/java/com/futo/circles/feature/group_invite/InviteMembersViewModel.kt @@ -1,11 +1,11 @@ package com.futo.circles.feature.group_invite -import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.futo.circles.extensions.launchUi import com.futo.circles.feature.group_invite.data_source.InviteMembersDataSource -import com.futo.circles.model.RoomMemberListItem +import com.futo.circles.model.InviteMemberListItem +import com.futo.circles.model.CirclesUser import kotlinx.coroutines.flow.* class InviteMembersViewModel( @@ -14,7 +14,7 @@ class InviteMembersViewModel( val titleLiveData = MutableLiveData(dataSource.getInviteTitle()) - val usersLiveData = MutableLiveData<List<RoomMemberListItem>>() + val usersLiveData = MutableLiveData<List<InviteMemberListItem>>() fun initSearchListener(queryFlow: StateFlow<String>) { launchUi { @@ -22,12 +22,12 @@ class InviteMembersViewModel( .debounce(500) .distinctUntilChanged() .flatMapLatest { query -> dataSource.search(query) } - .collectLatest { members -> - usersLiveData.postValue(members) - Log.d("MyLog", members.size.toString()) - } + .collectLatest { items -> usersLiveData.postValue(items) } } } + fun onUserSelected(member: CirclesUser){ + + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/feature/group_invite/data_source/InviteMembersDataSource.kt b/app/src/main/java/com/futo/circles/feature/group_invite/data_source/InviteMembersDataSource.kt index 258a426ea509cc34d01ddcc923ddb8902f33abdc..db6623d8befb41423e8176d10c98b10c50b49017 100644 --- a/app/src/main/java/com/futo/circles/feature/group_invite/data_source/InviteMembersDataSource.kt +++ b/app/src/main/java/com/futo/circles/feature/group_invite/data_source/InviteMembersDataSource.kt @@ -4,7 +4,10 @@ import android.content.Context import androidx.lifecycle.asFlow import com.futo.circles.R import com.futo.circles.extensions.nameOrId -import com.futo.circles.mapping.toRoomMember +import com.futo.circles.mapping.toCirclesUser +import com.futo.circles.model.HeaderItem +import com.futo.circles.model.InviteMemberListItem +import com.futo.circles.model.NoResultsItem import com.futo.circles.provider.MatrixSessionProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* @@ -27,7 +30,7 @@ class InviteMembersDataSource( suspend fun search(query: String) = combine(searchKnownUsers(query), searchSuggestions(query)) { knowUsers, suggestions -> - (knowUsers + suggestions).distinctBy { it.userId }.map { it.toRoomMember() } + buildList(knowUsers, suggestions) }.flowOn(Dispatchers.IO).distinctUntilChanged() @@ -46,9 +49,28 @@ class InviteMembersDataSource( emit(users ?: emptyList()) } + private fun buildList( + knowUsers: List<User>, + suggestions: List<User> + ): List<InviteMemberListItem> { + val list = mutableListOf<InviteMemberListItem>() + if (knowUsers.isNotEmpty()) { + list.add(HeaderItem.knownUsersHeader) + list.addAll(knowUsers.map { it.toCirclesUser() }) + } - private companion object { - private const val MAX_SUGGESTION_COUNT = 50 + val knowUsersIds = knowUsers.map { it.userId } + val filteredSuggestion = suggestions.filterNot { knowUsersIds.contains(it.userId) } + if (filteredSuggestion.isNotEmpty()) { + list.add(HeaderItem.suggestionHeader) + list.addAll(filteredSuggestion.map { it.toCirclesUser() }) + } + + if (list.isEmpty()) list.add(NoResultsItem()) + return list } + private companion object { + private const val MAX_SUGGESTION_COUNT = 25 + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMemberViewHolder.kt b/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMemberViewHolder.kt index c2894e761cdcabdf3075d21c81589a01dfeed6ee..b06a8a0156e07a206bccc6a8fe327d585559c294 100644 --- a/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMemberViewHolder.kt +++ b/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMemberViewHolder.kt @@ -1,22 +1,78 @@ package com.futo.circles.feature.group_invite.list +import android.util.Size +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.futo.circles.base.ViewBindingHolder -import com.futo.circles.databinding.InviteMemberListItemBinding -import com.futo.circles.model.RoomMemberListItem +import com.futo.circles.base.context +import com.futo.circles.databinding.InviteHeaderListItemBinding +import com.futo.circles.databinding.NoResultsListItemBinding +import com.futo.circles.databinding.UserListItemBinding +import com.futo.circles.extensions.loadImage +import com.futo.circles.extensions.onClick +import com.futo.circles.model.HeaderItem +import com.futo.circles.model.InviteMemberListItem +import com.futo.circles.model.NoResultsItem +import com.futo.circles.model.CirclesUser -class InviteMemberViewHolder( +abstract class InviteMemberViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract fun bind(data: InviteMemberListItem) +} + +class UserViewHolder( + parent: ViewGroup, + private val onMemberClicked: (Int) -> Unit +) : InviteMemberViewHolder(inflate(parent, UserListItemBinding::inflate)) { + + private companion object : ViewBindingHolder + + private val binding = baseBinding as UserListItemBinding + + init { + onClick(itemView) { position -> onMemberClicked(position) } + } + + override fun bind(data: InviteMemberListItem) { + if (data !is CirclesUser) return + + with(binding) { + ivUserImage.loadImage( + data.avatarUrl, + Size(ivUserImage.width, ivUserImage.height) + ) + tvUserName.text = data.name + tvUserId.text = data.id + } + } +} + +class HeaderViewHolder( parent: ViewGroup, -) : RecyclerView.ViewHolder(inflate(parent, InviteMemberListItemBinding::inflate)) { +) : InviteMemberViewHolder(inflate(parent, InviteHeaderListItemBinding::inflate)) { private companion object : ViewBindingHolder - private val binding = baseBinding as InviteMemberListItemBinding + private val binding = baseBinding as InviteHeaderListItemBinding + override fun bind(data: InviteMemberListItem) { + if (data !is HeaderItem) return - fun bind(data: RoomMemberListItem) { - binding.tvText.text = data.name + " / " + data.id + binding.tvHeader.text = context.getString(data.titleRes) } +} +class NoResultViewHolder( + parent: ViewGroup, +) : InviteMemberViewHolder(inflate(parent, NoResultsListItemBinding::inflate)) { + + private companion object : ViewBindingHolder + + private val binding = baseBinding as NoResultsListItemBinding + + override fun bind(data: InviteMemberListItem) { + if (data !is NoResultsItem) return + + binding.tvMessage.text = context.getString(data.titleRes) + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMembersListAdapter.kt b/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMembersListAdapter.kt index de81e0ebcd3d891e10d82befe324eb3d4da6dc04..bc396a40b2721c5ea3c31b5a7927a0a52210f788 100644 --- a/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMembersListAdapter.kt +++ b/app/src/main/java/com/futo/circles/feature/group_invite/list/InviteMembersListAdapter.kt @@ -2,17 +2,34 @@ package com.futo.circles.feature.group_invite.list import android.view.ViewGroup import com.futo.circles.base.BaseRvAdapter -import com.futo.circles.model.RoomMemberListItem +import com.futo.circles.model.HeaderItem +import com.futo.circles.model.InviteMemberListItem +import com.futo.circles.model.NoResultsItem +import com.futo.circles.model.CirclesUser -class InviteMembersListAdapter() : BaseRvAdapter<RoomMemberListItem, InviteMemberViewHolder>( +private enum class InviteListViewType { Header, User, NoResults } + +class InviteMembersListAdapter( + private val onUserSelected: (CirclesUser) -> Unit +) : BaseRvAdapter<InviteMemberListItem, InviteMemberViewHolder>( DefaultIdEntityCallback() ) { + override fun getItemViewType(position: Int): Int = when (getItem(position)) { + is HeaderItem -> InviteListViewType.Header.ordinal + is CirclesUser -> InviteListViewType.User.ordinal + is NoResultsItem -> InviteListViewType.NoResults.ordinal + } - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): InviteMemberViewHolder = InviteMemberViewHolder(parent = parent) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InviteMemberViewHolder { + return when (InviteListViewType.values()[viewType]) { + InviteListViewType.Header -> HeaderViewHolder(parent) + InviteListViewType.User -> UserViewHolder( + parent, + onMemberClicked = { position -> onUserSelected(getItem(position) as CirclesUser) }) + InviteListViewType.NoResults -> NoResultViewHolder(parent) + } + } override fun onBindViewHolder(holder: InviteMemberViewHolder, position: Int) { holder.bind(getItem(position)) diff --git a/app/src/main/java/com/futo/circles/feature/group_timeline/GroupTimelineFragment.kt b/app/src/main/java/com/futo/circles/feature/group_timeline/GroupTimelineFragment.kt index a4c3765a0ee17de01a9b035766a6c4c99f60076e..561ca3f5c57d9c638239625b798badae69ba71fa 100644 --- a/app/src/main/java/com/futo/circles/feature/group_timeline/GroupTimelineFragment.kt +++ b/app/src/main/java/com/futo/circles/feature/group_timeline/GroupTimelineFragment.kt @@ -40,7 +40,7 @@ class GroupTimelineFragment : Fragment(R.layout.group_timeline_fragment), GroupP super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) - binding.tvGroupTimeline.apply { + binding.rvGroupTimeline.apply { adapter = listAdapter addItemDecoration( BaseRvDecoration.OffsetDecoration<GroupPostViewHolder>( diff --git a/app/src/main/java/com/futo/circles/mapping/MatrixUserMapping.kt b/app/src/main/java/com/futo/circles/mapping/MatrixUserMapping.kt index a837f77804001e64997ffcd6735d4e0dd6369f9c..e66dcd10b579d5985b47b7d9afeb55b7cbc14d19 100644 --- a/app/src/main/java/com/futo/circles/mapping/MatrixUserMapping.kt +++ b/app/src/main/java/com/futo/circles/mapping/MatrixUserMapping.kt @@ -1,9 +1,9 @@ package com.futo.circles.mapping -import com.futo.circles.model.RoomMemberListItem +import com.futo.circles.model.CirclesUser import org.matrix.android.sdk.api.session.user.model.User -fun User.toRoomMember() = RoomMemberListItem( +fun User.toCirclesUser() = CirclesUser( id = userId, name = displayName ?: userId, avatarUrl = avatarUrl ?: "" diff --git a/app/src/main/java/com/futo/circles/model/InviteMemberListItem.kt b/app/src/main/java/com/futo/circles/model/InviteMemberListItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..ead4815d4a043fa357bd0097d0630998d32eb292 --- /dev/null +++ b/app/src/main/java/com/futo/circles/model/InviteMemberListItem.kt @@ -0,0 +1,30 @@ +package com.futo.circles.model + +import com.futo.circles.R +import com.futo.circles.base.IdEntity + +sealed class InviteMemberListItem : IdEntity<String> + +data class HeaderItem( + val titleRes: Int +) : InviteMemberListItem() { + override val id: String = titleRes.toString() + + companion object { + val knownUsersHeader = HeaderItem(R.string.known_users) + val suggestionHeader = HeaderItem(R.string.suggestion) + } +} + +data class CirclesUser( + override val id: String, + val name: String, + val avatarUrl: String, + val isSelected: Boolean = false +) : InviteMemberListItem() + +data class NoResultsItem( + val titleRes: Int = R.string.no_results +) : InviteMemberListItem() { + override val id: String = titleRes.toString() +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/model/RoomMemberListItem.kt b/app/src/main/java/com/futo/circles/model/RoomMemberListItem.kt deleted file mode 100644 index 4b236be0c3e323a9bcebef4fa738f2f76123b3e8..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/futo/circles/model/RoomMemberListItem.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.futo.circles.model - -import com.futo.circles.base.IdEntity - -data class RoomMemberListItem( - override val id: String, - val name: String, - val avatarUrl: String, - val isSelected: Boolean = false -) : IdEntity<String> \ No newline at end of file diff --git a/app/src/main/res/layout/group_timeline_fragment.xml b/app/src/main/res/layout/group_timeline_fragment.xml index 5873afd039dcc9ebdd03989cbf5efda09b0fd34d..30e82a65ad6f7169a3a65a0390d5db0dc1b5cab9 100644 --- a/app/src/main/res/layout/group_timeline_fragment.xml +++ b/app/src/main/res/layout/group_timeline_fragment.xml @@ -6,7 +6,7 @@ android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView - android:id="@+id/tvGroupTimeline" + android:id="@+id/rvGroupTimeline" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" @@ -21,7 +21,7 @@ android:layout_gravity="bottom|end" android:layout_margin="16dp" app:fabSize="normal" - app:layout_anchor="@id/tvGroupTimeline" + app:layout_anchor="@id/rvGroupTimeline" app:srcCompat="@drawable/ic_create" app:tint="@color/white" tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/invite_header_list_item.xml b/app/src/main/res/layout/invite_header_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..ab9cc4dd2bb658227980af656231d73578b91ddc --- /dev/null +++ b/app/src/main/res/layout/invite_header_list_item.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/tvHeader" + style="@style/headline" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="8dp" + tools:text="Header" /> \ No newline at end of file diff --git a/app/src/main/res/layout/invite_member_list_item.xml b/app/src/main/res/layout/invite_member_list_item.xml deleted file mode 100644 index 132aefc28e127e3cd8d9bb6581c7b6d158f0a81e..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/invite_member_list_item.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?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" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <TextView - android:id="@+id/tvText" - android:layout_width="0dp" - android:layout_height="wrap_content" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/invite_members_dialog_fragment.xml b/app/src/main/res/layout/invite_members_dialog_fragment.xml index cb3fc58240255f7c106b51f81380dc6e4ae6f522..fbd33cb6094e31f65d9a5384d346ce1c0e6a4cdf 100644 --- a/app/src/main/res/layout/invite_members_dialog_fragment.xml +++ b/app/src/main/res/layout/invite_members_dialog_fragment.xml @@ -41,7 +41,6 @@ android:id="@+id/rvUsers" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginTop="8dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/no_results_list_item.xml b/app/src/main/res/layout/no_results_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..fcff5997087593e09e2322bdeb203d1a1e609efb --- /dev/null +++ b/app/src/main/res/layout/no_results_list_item.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/tvMessage" + style="@style/headline" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:paddingVertical="48dp" + tools:text="@string/no_results"> + +</TextView> \ No newline at end of file diff --git a/app/src/main/res/layout/user_list_item.xml b/app/src/main/res/layout/user_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..4ce0814ac96ebee391879a552ce6c21aa39c8b7c --- /dev/null +++ b/app/src/main/res/layout/user_list_item.xml @@ -0,0 +1,51 @@ +<?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="?selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:paddingHorizontal="8dp" + android:paddingVertical="4dp"> + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/ivUserImage" + android:layout_width="40dp" + android:layout_height="0dp" + app:layout_constraintDimensionRatio="w,1:1" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent" + tools:background="@color/blue" /> + + <TextView + android:id="@+id/tvUserName" + style="@style/body" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintBottom_toTopOf="@id/tvUserId" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivUserImage" + app:layout_constraintTop_toTopOf="@id/ivUserImage" + app:layout_constraintVertical_chainStyle="packed" + tools:text="Android01" /> + + <TextView + android:id="@+id/tvUserId" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:lines="1" + android:textSize="13sp" + app:layout_constraintBottom_toBottomOf="@id/ivUserImage" + app:layout_constraintEnd_toEndOf="@id/tvUserName" + app:layout_constraintStart_toStartOf="@id/tvUserName" + app:layout_constraintTop_toBottomOf="@id/tvUserName" + tools:text="Android01@domain" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index fa720bdd17e56bd896cb64a24603d3b1fec935b4..83809b81d86cc5243f531c037a7e4bebb6c0b20d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,4 +4,5 @@ <dimen name="group_post_item_offset">4dp</dimen> <dimen name="reply_post_item_margin">24dp</dimen> <dimen name="post_text_side_margin">24dp</dimen> + <dimen name="invite_list_side_margin">8dp</dimen> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15530f2ebc5100e7cefe96332eadfe26da10408b..1853122428c5c6a5887322d0cdd86702cef01b92 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,9 @@ <string name="invite_members">Invite members</string> <string name="invite_members_to_format">Invite members to %s</string> <string name="search_by_name_or_id">Search by name or id</string> + <string name="known_users">Known users</string> + <string name="suggestion">Suggestions</string> + <string name="no_results">No results</string> <plurals name="member_plurals"> <item quantity="one">%d member</item>