WIP busqueda, hastags y pantalla de explorar perfiles

This commit is contained in:
erik-everardo 2024-03-31 22:50:06 -06:00
parent 8730bbe796
commit 49ccebb539
15 changed files with 337 additions and 30 deletions

View File

@ -0,0 +1,11 @@
package com.isolaatti.hashtags.presentation
import androidx.lifecycle.ViewModel
class HashtagsViewModel : ViewModel() {
}
class HashtagPostsViewModel : ViewModel() {
}

View File

@ -0,0 +1,39 @@
package com.isolaatti.hashtags.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.isolaatti.databinding.FragmentPostsHashtagBinding
import androidx.navigation.fragment.navArgs
class HashtagPostsFragment : Fragment() {
private lateinit var binding: FragmentPostsHashtagBinding
private val navArgs: HashtagPostsFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentPostsHashtagBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.title = "#${navArgs.hashtag}"
setupListeners()
}
private fun setupListeners() {
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
}
}

View File

@ -1,12 +0,0 @@
package com.isolaatti.hashtags.ui
import android.content.Context
import com.isolaatti.common.IsolaattiBaseActivity
class HashtagsActivity : IsolaattiBaseActivity() {
companion object {
fun startActivity(context: Context, hashtag: String? = null) {
}
}
}

View File

@ -0,0 +1,35 @@
package com.isolaatti.hashtags.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.isolaatti.databinding.FragmentHashtagsBinding
class HashtagsFragment : Fragment() {
private lateinit var binding: FragmentHashtagsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHashtagsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
}
private fun setupListeners() {
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
}
}

View File

@ -0,0 +1,6 @@
package com.isolaatti.profile.ui
import androidx.fragment.app.Fragment
class BrowseProfilesFragment : Fragment() {
}

View File

@ -5,8 +5,8 @@ import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
interface SearchApi { interface SearchApi {
@GET("Search/Quick") @GET("Search/v2")
fun quickSearch(@Query("q") query: String): Call<SearchDto> fun quickSearch(@Query("query") query: String): Call<SearchDto>
@GET("hashtags/trending") @GET("hashtags/trending")
fun getTrendingHashtags(): Call<HashtagsDto> fun getTrendingHashtags(): Call<HashtagsDto>

View File

@ -1,14 +1,33 @@
package com.isolaatti.search.data package com.isolaatti.search.data
import com.isolaatti.posting.posts.data.remote.FeedDto
enum class SearchResultType {
Profile, Post, Hashtag, Unknown
}
data class ProfileSearchDto(val id: Int, val name: String, val imageId: String?, val following: Boolean) data class ProfileSearchDto(val id: Int, val name: String, val imageId: String?, val following: Boolean)
data class SearchDto( data class SearchDto(val result: List<SearchResultDto>)
val profiles: List<ProfileSearchDto>,
val posts: List<FeedDto.PostDto> data class SearchResultDto(
// TODO add the other types val origin: String,
) val resourceId: String,
val title: String,
val description: String
) {
companion object {
const val ORIGIN_POSTS = "posts"
const val ORIGIN_HASHTAGS = "hashtags"
const val ORIGIN_USERS = "users"
}
val type: SearchResultType get() {
return when(origin) {
ORIGIN_POSTS -> SearchResultType.Post
ORIGIN_HASHTAGS -> SearchResultType.Hashtag
ORIGIN_USERS -> SearchResultType.Profile
else -> SearchResultType.Unknown
}
}
}
data class HashtagsDto(val result: List<String>) data class HashtagsDto(val result: List<String>)

View File

@ -0,0 +1,59 @@
package com.isolaatti.search.presentation
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load
import com.isolaatti.R
import com.isolaatti.databinding.SearchResultItemBinding
import com.isolaatti.search.data.SearchResultDto
import com.isolaatti.search.data.SearchResultType
import com.isolaatti.utils.UrlGen
class SearchResultsAdapter(
private val onItemClick: (item: SearchResultDto ) -> Unit = {}
) : ListAdapter<SearchResultDto, SearchResultsAdapter.SearchResultViewHolder>(itemCallback) {
companion object {
val itemCallback = object: DiffUtil.ItemCallback<SearchResultDto>() {
override fun areItemsTheSame(oldItem: SearchResultDto, newItem: SearchResultDto): Boolean {
return oldItem.resourceId == newItem.resourceId && oldItem.type == newItem.type
}
override fun areContentsTheSame(oldItem: SearchResultDto, newItem: SearchResultDto): Boolean {
return oldItem == newItem
}
}
}
inner class SearchResultViewHolder(val searchResultItemBinding: SearchResultItemBinding) : ViewHolder(searchResultItemBinding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchResultViewHolder {
return SearchResultViewHolder(SearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: SearchResultViewHolder, position: Int) {
val searchResult = getItem(position)
holder.searchResultItemBinding.apply {
searchResultTitle.text = searchResult.title
searchResultDescription.text = searchResult.description
root.setOnClickListener { onItemClick(searchResult) }
// TODO complete this
val image = when(searchResult.type) {
SearchResultType.Profile -> {
val userId = searchResult.resourceId.toIntOrNull()
if(userId != null) {
UrlGen.userProfileImage(userId)
} else {
R.drawable.baseline_search_24
}
}
else -> R.drawable.baseline_search_24
}
searchResultImage.load(image)
}
}
}

View File

@ -10,18 +10,21 @@ import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.carousel.CarouselLayoutManager import com.google.android.material.carousel.CarouselLayoutManager
import com.google.android.material.carousel.UncontainedCarouselStrategy import com.google.android.material.carousel.UncontainedCarouselStrategy
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentSearchBinding import com.isolaatti.databinding.FragmentSearchBinding
import com.isolaatti.hashtags.ui.HashtagsActivity
import com.isolaatti.profile.ui.ProfileActivity import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.search.data.HashtagsDto import com.isolaatti.search.data.HashtagsDto
import com.isolaatti.search.data.NewestUsersDto import com.isolaatti.search.data.NewestUsersDto
import com.isolaatti.search.data.SearchDto import com.isolaatti.search.data.SearchDto
import com.isolaatti.search.data.SearchHistoryEntity import com.isolaatti.search.data.SearchHistoryEntity
import com.isolaatti.search.data.SearchResultType
import com.isolaatti.search.presentation.SearchResultsAdapter
import com.isolaatti.search.presentation.SearchSuggestionsAdapter import com.isolaatti.search.presentation.SearchSuggestionsAdapter
import com.isolaatti.search.presentation.SearchViewModel import com.isolaatti.search.presentation.SearchViewModel
import com.isolaatti.search.presentation.UserCarouselAdapter import com.isolaatti.search.presentation.UserCarouselAdapter
@ -38,6 +41,7 @@ class SearchFragment : Fragment() {
private val viewModel: SearchViewModel by viewModels() private val viewModel: SearchViewModel by viewModels()
private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null
private var newestUsersAdapter: UserCarouselAdapter? = null private var newestUsersAdapter: UserCarouselAdapter? = null
private var searchResultsAdapter: SearchResultsAdapter? = null
private val searchSuggestionsObserver: Observer<List<SearchHistoryEntity>> = Observer { private val searchSuggestionsObserver: Observer<List<SearchHistoryEntity>> = Observer {
searchSuggestionsAdapter?.submitList(it) searchSuggestionsAdapter?.submitList(it)
@ -45,6 +49,7 @@ class SearchFragment : Fragment() {
private val searchResultsObserver: Observer<SearchDto> = Observer { private val searchResultsObserver: Observer<SearchDto> = Observer {
Log.d(LOG_TAG, it.toString()) Log.d(LOG_TAG, it.toString())
searchResultsAdapter?.submitList(it.result)
} }
private val trendingHashtagsObserver: Observer<HashtagsDto> = Observer { private val trendingHashtagsObserver: Observer<HashtagsDto> = Observer {
@ -55,7 +60,7 @@ class SearchFragment : Fragment() {
viewBinding.chipGroup.addView(Chip(requireContext()).apply { viewBinding.chipGroup.addView(Chip(requireContext()).apply {
text = "#$hashtag" text = "#$hashtag"
setOnClickListener { setOnClickListener {
requireContext().startActivity(Intent(requireContext(), HashtagsActivity::class.java)) findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagPostsFragment(hashtag))
} }
}) })
} }
@ -123,7 +128,18 @@ class SearchFragment : Fragment() {
layoutManager = CarouselLayoutManager(UncontainedCarouselStrategy()) layoutManager = CarouselLayoutManager(UncontainedCarouselStrategy())
} }
searchResultsAdapter = SearchResultsAdapter(
onItemClick = {
when(it.type) {
SearchResultType.Profile -> {}
SearchResultType.Post -> {}
SearchResultType.Hashtag -> {}
SearchResultType.Unknown -> {}
}
}
)
viewBinding.recyclerViewSearchResults.adapter = searchResultsAdapter
viewBinding.recyclerViewSearchResults.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
} }
private fun setupListeners() { private fun setupListeners() {
@ -147,11 +163,19 @@ class SearchFragment : Fragment() {
if(it.itemId == R.id.close_button) { if(it.itemId == R.id.close_button) {
showResults(false) showResults(false)
true true
} else {
false
} }
false
}
viewBinding.openHashtagsButton.setOnClickListener {
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagsFragment())
}
viewBinding.browseProfilesButton.setOnClickListener {
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToBrowseProfilesFragment())
} }
viewBinding.searchView.addTransitionListener { searchView, transitionState, transitionState2 -> }
} }
private fun setupObservers() { private fun setupObservers() {

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/baseline_close_24"
app:title="@string/hashtags"/>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/baseline_arrow_back_24"
app:titleCentered="true"/>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -52,11 +52,12 @@
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/open_hashtags_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@id/chip_group" app:layout_constraintTop_toBottomOf="@id/chip_group"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/see_all"/> android:text="@string/go_to_hashtags"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
@ -90,11 +91,12 @@
app:layout_constraintTop_toBottomOf="@id/new_users_card_title"/> app:layout_constraintTop_toBottomOf="@id/new_users_card_title"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/browse_profiles_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@id/new_users_recycler_view" app:layout_constraintTop_toBottomOf="@id/new_users_recycler_view"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/see_all"/> android:text="@string/browse_profiles"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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="70dp"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="4dp"
style="?attr/materialCardViewFilledStyle">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/search_result_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/baseline_search_24"/>
<TextView
android:id="@+id/search_result_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/search_result_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/search_result_image"
app:layout_constraintTop_toTopOf="parent"
android:maxLines="1"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
tools:text="Title" />
<TextView
android:id="@+id/search_result_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/search_result_image"
app:layout_constraintTop_toBottomOf="@+id/search_result_title"
android:maxLines="1"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
tools:text="Description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -30,5 +30,35 @@
<fragment <fragment
android:id="@+id/searchFragment" android:id="@+id/searchFragment"
android:name="com.isolaatti.search.ui.SearchFragment" android:name="com.isolaatti.search.ui.SearchFragment"
android:label="@string/search" /> android:label="@string/search" >
<action
android:id="@+id/action_searchFragment_to_hashtagsFragment"
app:destination="@id/hashtagsFragment" />
<action
android:id="@+id/action_searchFragment_to_hashtagPostsFragment"
app:destination="@id/hashtagPostsFragment2" />
<action
android:id="@+id/action_searchFragment_to_browseProfilesFragment"
app:destination="@id/browseProfilesFragment" />
</fragment>
<fragment
android:id="@+id/hashtagsFragment"
android:name="com.isolaatti.hashtags.ui.HashtagsFragment"
android:label="HashtagsFragment" >
<action
android:id="@+id/action_hashtagsFragment2_to_hashtagPostsFragment2"
app:destination="@id/hashtagPostsFragment2" />
</fragment>
<fragment
android:id="@+id/hashtagPostsFragment2"
android:name="com.isolaatti.hashtags.ui.HashtagPostsFragment"
android:label="HashtagPostsFragment" >
<argument
android:name="hashtag"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/browseProfilesFragment"
android:name="com.isolaatti.profile.ui.BrowseProfilesFragment"
android:label="BrowseProfilesFragment" />
</navigation> </navigation>

View File

@ -201,4 +201,6 @@
<string name="hashtags">Hashtags</string> <string name="hashtags">Hashtags</string>
<string name="newest_profiles">Newest profiles</string> <string name="newest_profiles">Newest profiles</string>
<string name="see_all">See all</string> <string name="see_all">See all</string>
<string name="go_to_hashtags">Go to hashtags</string>
<string name="browse_profiles">Browse profiles</string>
</resources> </resources>