From 49ccebb5395d8ec253a39fd01463e25f4a60e9e6 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sun, 31 Mar 2024 22:50:06 -0600 Subject: [PATCH 01/10] WIP busqueda, hastags y pantalla de explorar perfiles --- .../presentation/HashtagsViewModels.kt | 11 ++++ .../hashtags/ui/HashtagPostsFragment.kt | 39 ++++++++++++ .../isolaatti/hashtags/ui/HashtagsActivity.kt | 12 ---- .../isolaatti/hashtags/ui/HashtagsFragment.kt | 35 +++++++++++ .../profile/ui/BrowseProfilesFragment.kt | 6 ++ .../com/isolaatti/search/data/SearchApi.kt | 4 +- .../com/isolaatti/search/data/SearchDto.kt | 33 ++++++++--- .../presentation/SearchResultsAdapter.kt | 59 +++++++++++++++++++ .../com/isolaatti/search/ui/SearchFragment.kt | 36 +++++++++-- app/src/main/res/layout/fragment_hashtags.xml | 17 ++++++ .../res/layout/fragment_posts_hashtag.xml | 18 ++++++ app/src/main/res/layout/fragment_search.xml | 6 +- .../main/res/layout/search_result_item.xml | 57 ++++++++++++++++++ .../main/res/navigation/home_navigation.xml | 32 +++++++++- app/src/main/res/values/strings.xml | 2 + 15 files changed, 337 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt create mode 100644 app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt delete mode 100644 app/src/main/java/com/isolaatti/hashtags/ui/HashtagsActivity.kt create mode 100644 app/src/main/java/com/isolaatti/hashtags/ui/HashtagsFragment.kt create mode 100644 app/src/main/java/com/isolaatti/profile/ui/BrowseProfilesFragment.kt create mode 100644 app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt create mode 100644 app/src/main/res/layout/fragment_hashtags.xml create mode 100644 app/src/main/res/layout/fragment_posts_hashtag.xml create mode 100644 app/src/main/res/layout/search_result_item.xml diff --git a/app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt b/app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt new file mode 100644 index 0000000..33ad357 --- /dev/null +++ b/app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt @@ -0,0 +1,11 @@ +package com.isolaatti.hashtags.presentation + +import androidx.lifecycle.ViewModel + +class HashtagsViewModel : ViewModel() { + +} + +class HashtagPostsViewModel : ViewModel() { + +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt b/app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt new file mode 100644 index 0000000..0763734 --- /dev/null +++ b/app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/hashtags/ui/HashtagsActivity.kt b/app/src/main/java/com/isolaatti/hashtags/ui/HashtagsActivity.kt deleted file mode 100644 index 970447c..0000000 --- a/app/src/main/java/com/isolaatti/hashtags/ui/HashtagsActivity.kt +++ /dev/null @@ -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) { - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/hashtags/ui/HashtagsFragment.kt b/app/src/main/java/com/isolaatti/hashtags/ui/HashtagsFragment.kt new file mode 100644 index 0000000..346a1ca --- /dev/null +++ b/app/src/main/java/com/isolaatti/hashtags/ui/HashtagsFragment.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/ui/BrowseProfilesFragment.kt b/app/src/main/java/com/isolaatti/profile/ui/BrowseProfilesFragment.kt new file mode 100644 index 0000000..44524e3 --- /dev/null +++ b/app/src/main/java/com/isolaatti/profile/ui/BrowseProfilesFragment.kt @@ -0,0 +1,6 @@ +package com.isolaatti.profile.ui + +import androidx.fragment.app.Fragment + +class BrowseProfilesFragment : Fragment() { +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/search/data/SearchApi.kt b/app/src/main/java/com/isolaatti/search/data/SearchApi.kt index 86e237c..273da9d 100644 --- a/app/src/main/java/com/isolaatti/search/data/SearchApi.kt +++ b/app/src/main/java/com/isolaatti/search/data/SearchApi.kt @@ -5,8 +5,8 @@ import retrofit2.http.GET import retrofit2.http.Query interface SearchApi { - @GET("Search/Quick") - fun quickSearch(@Query("q") query: String): Call + @GET("Search/v2") + fun quickSearch(@Query("query") query: String): Call @GET("hashtags/trending") fun getTrendingHashtags(): Call diff --git a/app/src/main/java/com/isolaatti/search/data/SearchDto.kt b/app/src/main/java/com/isolaatti/search/data/SearchDto.kt index fbf44c6..d68f296 100644 --- a/app/src/main/java/com/isolaatti/search/data/SearchDto.kt +++ b/app/src/main/java/com/isolaatti/search/data/SearchDto.kt @@ -1,14 +1,33 @@ 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 SearchDto( - val profiles: List, - val posts: List - // TODO add the other types -) +data class SearchDto(val result: List) + +data class SearchResultDto( + 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) diff --git a/app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt b/app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt new file mode 100644 index 0000000..669e458 --- /dev/null +++ b/app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt @@ -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(itemCallback) { + companion object { + val itemCallback = object: DiffUtil.ItemCallback() { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt b/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt index d67e85d..37e4101 100644 --- a/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt +++ b/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt @@ -10,18 +10,21 @@ import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Observer +import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.carousel.CarouselLayoutManager import com.google.android.material.carousel.UncontainedCarouselStrategy import com.google.android.material.chip.Chip import com.isolaatti.R import com.isolaatti.databinding.FragmentSearchBinding -import com.isolaatti.hashtags.ui.HashtagsActivity import com.isolaatti.profile.ui.ProfileActivity import com.isolaatti.search.data.HashtagsDto import com.isolaatti.search.data.NewestUsersDto import com.isolaatti.search.data.SearchDto 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.SearchViewModel import com.isolaatti.search.presentation.UserCarouselAdapter @@ -38,6 +41,7 @@ class SearchFragment : Fragment() { private val viewModel: SearchViewModel by viewModels() private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null private var newestUsersAdapter: UserCarouselAdapter? = null + private var searchResultsAdapter: SearchResultsAdapter? = null private val searchSuggestionsObserver: Observer> = Observer { searchSuggestionsAdapter?.submitList(it) @@ -45,6 +49,7 @@ class SearchFragment : Fragment() { private val searchResultsObserver: Observer = Observer { Log.d(LOG_TAG, it.toString()) + searchResultsAdapter?.submitList(it.result) } private val trendingHashtagsObserver: Observer = Observer { @@ -55,7 +60,7 @@ class SearchFragment : Fragment() { viewBinding.chipGroup.addView(Chip(requireContext()).apply { text = "#$hashtag" setOnClickListener { - requireContext().startActivity(Intent(requireContext(), HashtagsActivity::class.java)) + findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagPostsFragment(hashtag)) } }) } @@ -123,7 +128,18 @@ class SearchFragment : Fragment() { 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() { @@ -147,11 +163,19 @@ class SearchFragment : Fragment() { if(it.itemId == R.id.close_button) { showResults(false) 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() { diff --git a/app/src/main/res/layout/fragment_hashtags.xml b/app/src/main/res/layout/fragment_hashtags.xml new file mode 100644 index 0000000..cb65f9e --- /dev/null +++ b/app/src/main/res/layout/fragment_hashtags.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_posts_hashtag.xml b/app/src/main/res/layout/fragment_posts_hashtag.xml new file mode 100644 index 0000000..8748d07 --- /dev/null +++ b/app/src/main/res/layout/fragment_posts_hashtag.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index c253351..22024ba 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -52,11 +52,12 @@ + android:text="@string/go_to_hashtags"/> + android:text="@string/browse_profiles"/> diff --git a/app/src/main/res/layout/search_result_item.xml b/app/src/main/res/layout/search_result_item.xml new file mode 100644 index 0000000..014b567 --- /dev/null +++ b/app/src/main/res/layout/search_result_item.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/home_navigation.xml b/app/src/main/res/navigation/home_navigation.xml index a233317..b6349d4 100644 --- a/app/src/main/res/navigation/home_navigation.xml +++ b/app/src/main/res/navigation/home_navigation.xml @@ -30,5 +30,35 @@ + android:label="@string/search" > + + + + + + + + + + + \ 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 031087b..3fe0e28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -201,4 +201,6 @@ Hashtags Newest profiles See all + Go to hashtags + Browse profiles \ No newline at end of file From 000937ded47c999555e6804719b764fa92ebfc5b Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Wed, 3 Apr 2024 19:48:01 -0600 Subject: [PATCH 02/10] WIP busqueda: abre resultados de busqueda --- .../java/com/isolaatti/search/ui/SearchFragment.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt b/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt index 37e4101..29b393e 100644 --- a/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt +++ b/app/src/main/java/com/isolaatti/search/ui/SearchFragment.kt @@ -18,6 +18,7 @@ import com.google.android.material.carousel.UncontainedCarouselStrategy import com.google.android.material.chip.Chip import com.isolaatti.R import com.isolaatti.databinding.FragmentSearchBinding +import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity import com.isolaatti.profile.ui.ProfileActivity import com.isolaatti.search.data.HashtagsDto import com.isolaatti.search.data.NewestUsersDto @@ -131,9 +132,15 @@ class SearchFragment : Fragment() { searchResultsAdapter = SearchResultsAdapter( onItemClick = { when(it.type) { - SearchResultType.Profile -> {} - SearchResultType.Post -> {} - SearchResultType.Hashtag -> {} + SearchResultType.Profile -> { + it.resourceId.toIntOrNull()?.also { ProfileActivity.startActivity(requireContext(), it) } + } + SearchResultType.Post -> { + it.resourceId.toLongOrNull()?.also { PostViewerActivity.startActivity(requireContext(), it) } + } + SearchResultType.Hashtag -> { + findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagPostsFragment(it.resourceId)) + } SearchResultType.Unknown -> {} } } From 3ef8961729f2c6e48899aacfc74ead7e120d1230 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sat, 6 Apr 2024 19:55:25 -0600 Subject: [PATCH 03/10] busqueda casi completa, refactorizacion, iconos nuevos en resultados, posts de hashtag --- app/src/main/AndroidManifest.xml | 6 +- .../main/java/com/isolaatti/MainActivity.kt | 2 +- .../isolaatti/common/IsolaattiBaseActivity.kt | 3 - .../isolaatti/hashtags/data/HashtagsApi.kt | 12 + .../presentation/HashtagsViewModels.kt | 11 - .../hashtags/ui/HashtagPostsFragment.kt | 7 + .../home/presentation/FeedViewModel.kt | 42 +--- .../com/isolaatti/home/ui/FeedFragment.kt | 140 +++++++++++ .../isolaatti/home/{ => ui}/HomeActivity.kt | 6 +- .../posting/posts/data/remote/FeedsApi.kt | 4 + .../data/repository/PostsRepositoryImpl.kt | 16 +- .../posting/posts/domain/PostsRepository.kt | 2 +- .../presentation/PostListingViewModel.kt | 48 ++++ .../presentation/PostListingViewModelBase.kt | 2 +- .../posts/ui/PostListingFragment.kt} | 236 ++++++------------ .../profile/presentation/ProfileViewModel.kt | 4 +- .../profile/ui/ProfileMainFragment.kt | 6 +- .../presentation/SearchResultsAdapter.kt | 2 + .../sign_up/ui/MakeAccountFragment.kt | 2 +- .../main/res/drawable/baseline_article_24.xml | 5 + app/src/main/res/drawable/hashtag_solid.xml | 9 + .../main/res/layout-land/fragment_feed.xml | 25 +- app/src/main/res/layout/fragment_feed.xml | 23 +- .../main/res/layout/fragment_post_listing.xml | 24 ++ .../res/layout/fragment_posts_hashtag.xml | 8 + .../main/res/navigation/home_navigation.xml | 4 +- .../navigation/post_listing_navigation.xml | 16 ++ 27 files changed, 390 insertions(+), 275 deletions(-) create mode 100644 app/src/main/java/com/isolaatti/hashtags/data/HashtagsApi.kt delete mode 100644 app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt create mode 100644 app/src/main/java/com/isolaatti/home/ui/FeedFragment.kt rename app/src/main/java/com/isolaatti/home/{ => ui}/HomeActivity.kt (96%) create mode 100644 app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModel.kt rename app/src/main/java/com/isolaatti/{home/FeedFragment.kt => posting/posts/ui/PostListingFragment.kt} (61%) create mode 100644 app/src/main/res/drawable/baseline_article_24.xml create mode 100644 app/src/main/res/drawable/hashtag_solid.xml create mode 100644 app/src/main/res/layout/fragment_post_listing.xml create mode 100644 app/src/main/res/navigation/post_listing_navigation.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e2c6abd..0176c48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,15 +26,15 @@ - + + android:parentActivityName=".home.ui.HomeActivity"/> + android:parentActivityName=".home.ui.HomeActivity"/> diff --git a/app/src/main/java/com/isolaatti/MainActivity.kt b/app/src/main/java/com/isolaatti/MainActivity.kt index 04a494f..213d5f8 100644 --- a/app/src/main/java/com/isolaatti/MainActivity.kt +++ b/app/src/main/java/com/isolaatti/MainActivity.kt @@ -7,7 +7,7 @@ import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.isolaatti.auth.data.AuthRepositoryImpl -import com.isolaatti.home.HomeActivity +import com.isolaatti.home.ui.HomeActivity import com.isolaatti.login.LogInActivity import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject diff --git a/app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt b/app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt index 4d2dad3..c087d50 100644 --- a/app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt +++ b/app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.os.Bundle import android.util.Log import android.view.View -import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -14,9 +13,7 @@ import androidx.lifecycle.Observer import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.isolaatti.R -import com.isolaatti.connectivity.ConnectivityCallbackImpl import com.isolaatti.connectivity.NetworkStatus -import com.isolaatti.home.HomeActivity import com.isolaatti.login.LogInActivity import com.isolaatti.utils.Resource import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/com/isolaatti/hashtags/data/HashtagsApi.kt b/app/src/main/java/com/isolaatti/hashtags/data/HashtagsApi.kt new file mode 100644 index 0000000..76960b5 --- /dev/null +++ b/app/src/main/java/com/isolaatti/hashtags/data/HashtagsApi.kt @@ -0,0 +1,12 @@ +package com.isolaatti.hashtags.data + +import com.isolaatti.common.ResultDto +import com.isolaatti.posting.posts.data.remote.FeedDto +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface HashtagsApi { + +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt b/app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt deleted file mode 100644 index 33ad357..0000000 --- a/app/src/main/java/com/isolaatti/hashtags/presentation/HashtagsViewModels.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.isolaatti.hashtags.presentation - -import androidx.lifecycle.ViewModel - -class HashtagsViewModel : ViewModel() { - -} - -class HashtagPostsViewModel : ViewModel() { - -} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt b/app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt index 0763734..6194d7f 100644 --- a/app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt +++ b/app/src/main/java/com/isolaatti/hashtags/ui/HashtagPostsFragment.kt @@ -5,9 +5,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import com.isolaatti.databinding.FragmentPostsHashtagBinding import androidx.navigation.fragment.navArgs +import com.isolaatti.R +import com.isolaatti.posting.posts.ui.PostListingFragment class HashtagPostsFragment : Fragment() { @@ -29,6 +32,10 @@ class HashtagPostsFragment : Fragment() { binding.toolbar.title = "#${navArgs.hashtag}" setupListeners() + + // show feed + (childFragmentManager.findFragmentById(R.id.post_list_fragment_container) as NavHostFragment) + .navController.setGraph(R.navigation.post_listing_navigation, Bundle().apply { putString(PostListingFragment.ARG_HASHTAG, navArgs.hashtag) }) } private fun setupListeners() { diff --git a/app/src/main/java/com/isolaatti/home/presentation/FeedViewModel.kt b/app/src/main/java/com/isolaatti/home/presentation/FeedViewModel.kt index bf5cece..fb9a27a 100644 --- a/app/src/main/java/com/isolaatti/home/presentation/FeedViewModel.kt +++ b/app/src/main/java/com/isolaatti/home/presentation/FeedViewModel.kt @@ -2,12 +2,12 @@ package com.isolaatti.home.presentation import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.presentation.PostListingViewModelBase import com.isolaatti.posting.posts.presentation.UpdateEvent -import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.domain.entity.UserProfile import com.isolaatti.profile.domain.use_case.GetProfile import com.isolaatti.utils.Resource @@ -22,9 +22,8 @@ import javax.inject.Inject @HiltViewModel class FeedViewModel @Inject constructor( private val getProfileUseCase: GetProfile, - private val authRepository: AuthRepository, - private val postsRepository: PostsRepository -) : PostListingViewModelBase() { + private val authRepository: AuthRepository +) : ViewModel() { private val toRetry: MutableList = mutableListOf() @@ -39,41 +38,7 @@ class FeedViewModel @Inject constructor( toRetry.clear() } - override fun getFeed(refresh: Boolean) { - viewModelScope.launch { - if (refresh) { - posts.value = null - } - postsRepository.getFeed(getLastId()).onEach { listResource -> - when (listResource) { - is Resource.Success -> { - val eventType = if((postsList?.size ?: 0) > 0) UpdateEvent.UpdateType.PAGE_ADDED else UpdateEvent.UpdateType.REFRESH - loadingPosts.postValue(false) - posts.postValue(Pair(postsList?.apply { - addAll(listResource.data ?: listOf()) - } ?: listResource.data, - UpdateEvent(eventType, null))) - noMoreContent.postValue(listResource.data?.size == 0) - } - - is Resource.Loading -> { - if(!refresh) - loadingPosts.postValue(true) - } - - is Resource.Error -> { - errorLoading.postValue(listResource.errorType) - toRetry.add { - getFeed(refresh) - } - } - - } - isLoadingFromScrolling = false - }.flowOn(Dispatchers.IO).launchIn(this) - } - } // User profile private val _userProfile: MutableLiveData = MutableLiveData() @@ -88,7 +53,6 @@ class FeedViewModel @Inject constructor( when(profile) { is Resource.Error -> { - errorLoading.postValue(profile.errorType) toRetry.add { getProfile() } diff --git a/app/src/main/java/com/isolaatti/home/ui/FeedFragment.kt b/app/src/main/java/com/isolaatti/home/ui/FeedFragment.kt new file mode 100644 index 0000000..f22f9e6 --- /dev/null +++ b/app/src/main/java/com/isolaatti/home/ui/FeedFragment.kt @@ -0,0 +1,140 @@ +package com.isolaatti.home.ui + +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import coil.load +import com.google.android.material.card.MaterialCardView +import com.isolaatti.R +import com.isolaatti.about.AboutActivity +import com.isolaatti.common.CoilImageLoader.imageLoader +import com.isolaatti.common.ErrorMessageViewModel +import com.isolaatti.databinding.FragmentFeedBinding +import com.isolaatti.home.presentation.FeedViewModel +import com.isolaatti.posting.posts.presentation.CreatePostContract +import com.isolaatti.posting.posts.ui.PostListingFragment +import com.isolaatti.profile.ui.ProfileActivity +import com.isolaatti.settings.ui.SettingsActivity +import com.isolaatti.utils.UrlGen +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class FeedFragment : Fragment() { + + companion object { + fun newInstance() = FeedFragment() + const val CALLER_ID = 20 + } + + private val errorViewModel: ErrorMessageViewModel by activityViewModels() + private val viewModel: FeedViewModel by activityViewModels() + + private var currentUserId = 0 + + private lateinit var viewBinding: FragmentFeedBinding + + private val createDiscussion = registerForActivityResult(CreatePostContract()) { + if(it != null) { + Toast.makeText(requireContext(), R.string.posted_successfully, Toast.LENGTH_SHORT).show() + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + viewBinding = FragmentFeedBinding.inflate(inflater) + + return viewBinding.root + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewBinding.topAppBar.setNavigationOnClickListener { + viewBinding.drawerLayout?.openDrawer(viewBinding.homeDrawer) + } + + + viewBinding.homeDrawer.setNavigationItemSelectedListener { + when(it.itemId) { + R.id.settings_menu_item -> { + startActivity(Intent(requireActivity(), SettingsActivity::class.java)) + true + } + R.id.about_menu_item -> { + startActivity(Intent(requireActivity(), AboutActivity::class.java)) + true + } + else -> {true} + } + } + + + viewBinding.topAppBar.setOnMenuItemClickListener { + when(it.itemId) { + R.id.menu_item_new_discussion -> { + createDiscussion.launch(Unit) + true + } + else -> { + false + } + } + } + + // show feed + + (childFragmentManager.findFragmentById(R.id.post_list_fragment_container) as NavHostFragment).navController.setGraph(R.navigation.post_listing_navigation) + + viewModel.userProfile.observe(viewLifecycleOwner) { + val header = viewBinding.homeDrawer.getHeaderView(0) as? ConstraintLayout + + val image: ImageView? = header?.findViewById(R.id.profileImageView) + val textViewName: TextView? = header?.findViewById(R.id.textViewName) + val textViewEmail: TextView? = header?.findViewById(R.id.textViewEmail) + val textViewUsername: TextView? = header?.findViewById(R.id.textViewUsername) + + image?.load(UrlGen.userProfileImage(it.userId), imageLoader) + + val card: MaterialCardView? = header?.findViewById(R.id.drawer_header_card) + card?.setOnClickListener { + ProfileActivity.startActivity(requireContext(), currentUserId) + } + + textViewName?.text = it.name + textViewEmail?.text = it.email + textViewUsername?.text = "@${it.uniqueUsername}" + currentUserId = it.userId + } + + + + + viewModel.getProfile() + + + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + errorViewModel.retry.collect { + viewModel.retry() + errorViewModel.handleRetry() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/HomeActivity.kt b/app/src/main/java/com/isolaatti/home/ui/HomeActivity.kt similarity index 96% rename from app/src/main/java/com/isolaatti/home/HomeActivity.kt rename to app/src/main/java/com/isolaatti/home/ui/HomeActivity.kt index dd593be..9fe8c59 100644 --- a/app/src/main/java/com/isolaatti/home/HomeActivity.kt +++ b/app/src/main/java/com/isolaatti/home/ui/HomeActivity.kt @@ -1,4 +1,4 @@ -package com.isolaatti.home +package com.isolaatti.home.ui import android.Manifest import android.content.SharedPreferences @@ -45,10 +45,6 @@ class HomeActivity : IsolaattiBaseActivity() { askNotificationPermission() - if(savedInstanceState == null) { - feedViewModel.getFeed(false) - } - } override fun onCreateOptionsMenu(menu: Menu?): Boolean { diff --git a/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedsApi.kt b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedsApi.kt index 6ab7fae..3fd95c2 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedsApi.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedsApi.kt @@ -1,5 +1,6 @@ package com.isolaatti.posting.posts.data.remote +import com.isolaatti.common.ResultDto import com.isolaatti.profile.data.remote.ProfileListItemDto import retrofit2.Call import retrofit2.http.GET @@ -22,4 +23,7 @@ interface FeedsApi { @GET("Feed") fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call + + @GET("hashtags/hashtag/{hashtag}") + fun getHashtagPosts(@Path("hashtag") hashtag: String, @Query("after") afterPost: Long? = null): Call } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt b/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt index 236f014..5867bb9 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.isolaatti.posting.posts.data.repository +import android.util.Log import com.google.gson.Gson import com.isolaatti.posting.posts.data.remote.CreatePostDto import com.isolaatti.posting.posts.data.remote.DeletePostDto @@ -18,10 +19,18 @@ import retrofit2.awaitResponse import javax.inject.Inject class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository { - override fun getFeed(lastId: Long): Flow>> = flow { + companion object { + const val LOG_TAG = "PostsRepositoryImpl" + } + override fun getFeed(lastId: Long, hashtag: String?): Flow>> = flow { emit(Resource.Loading()) try { - val result = feedsApi.getChronology(lastId, 20).execute() + + val result = if(hashtag == null) { + feedsApi.getChronology(lastId, 20).awaitResponse() + } else { + feedsApi.getHashtagPosts(hashtag, lastId).awaitResponse() + } if(result.isSuccessful) { emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) })) return@flow @@ -32,7 +41,8 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr 500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError)) else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError)) } - } catch(_: Exception) { + } catch(e: Exception) { + Log.e(LOG_TAG, "Error $e") emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) } } diff --git a/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt b/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt index 8ac02c7..a5900ab 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow interface PostsRepository { - fun getFeed(lastId: Long): Flow>> + fun getFeed(lastId: Long, hashtag: String?): Flow>> fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow>> diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModel.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModel.kt new file mode 100644 index 0000000..4fb6e28 --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModel.kt @@ -0,0 +1,48 @@ +package com.isolaatti.posting.posts.presentation + +import androidx.lifecycle.viewModelScope +import com.isolaatti.posting.posts.domain.PostsRepository +import com.isolaatti.utils.Resource +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PostListingViewModel @Inject constructor(private val postsRepository: PostsRepository) : PostListingViewModelBase() { + override fun getFeed(refresh: Boolean, hashtag: String?) { + viewModelScope.launch { + if (refresh) { + posts.value = null + } + postsRepository.getFeed(getLastId(), hashtag).onEach { listResource -> + when (listResource) { + is Resource.Success -> { + val eventType = if((postsList?.size ?: 0) > 0) UpdateEvent.UpdateType.PAGE_ADDED else UpdateEvent.UpdateType.REFRESH + loadingPosts.postValue(false) + posts.postValue(Pair(postsList?.apply { + addAll(listResource.data ?: listOf()) + } ?: listResource.data, + UpdateEvent(eventType, null))) + + noMoreContent.postValue(listResource.data?.size == 0) + } + + is Resource.Loading -> { + if(!refresh) + loadingPosts.postValue(true) + } + + is Resource.Error -> { + errorLoading.postValue(listResource.errorType) + } + + } + isLoadingFromScrolling = false + }.flowOn(Dispatchers.IO).launchIn(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt index 59c1319..8303f1d 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostListingViewModelBase.kt @@ -44,7 +44,7 @@ abstract class PostListingViewModelBase : ViewModel() { fun getLastId(): Long = try { posts.value?.first?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 } - abstract fun getFeed(refresh: Boolean) + abstract fun getFeed(refresh: Boolean, hashtag: String?) fun likePost(postId: Long) { viewModelScope.launch { diff --git a/app/src/main/java/com/isolaatti/home/FeedFragment.kt b/app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt similarity index 61% rename from app/src/main/java/com/isolaatti/home/FeedFragment.kt rename to app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt index a281b72..805dd67 100644 --- a/app/src/main/java/com/isolaatti/home/FeedFragment.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt @@ -1,52 +1,38 @@ -package com.isolaatti.home +package com.isolaatti.posting.posts.ui -import android.content.Intent import android.os.Bundle -import androidx.fragment.app.Fragment +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import android.widget.Toast -import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle +import androidx.fragment.app.viewModels import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager -import coil.load -import com.google.android.material.button.MaterialButton -import com.google.android.material.card.MaterialCardView import com.isolaatti.BuildConfig -import com.isolaatti.R -import com.isolaatti.about.AboutActivity import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.common.domain.Playable import com.isolaatti.audio.player.AudioPlayerConnector -import com.isolaatti.common.CoilImageLoader.imageLoader +import com.isolaatti.common.CoilImageLoader import com.isolaatti.common.Dialogs import com.isolaatti.common.ErrorMessageViewModel -import com.isolaatti.databinding.FragmentFeedBinding -import com.isolaatti.drafts.ui.DraftsActivity -import com.isolaatti.home.presentation.FeedViewModel -import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity -import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity -import com.isolaatti.posting.comments.ui.BottomSheetPostComments import com.isolaatti.common.OnUserInteractedWithPostCallback import com.isolaatti.common.Ownable import com.isolaatti.common.options_bottom_sheet.domain.OptionClicked import com.isolaatti.common.options_bottom_sheet.domain.Options import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment +import com.isolaatti.databinding.FragmentPostListingBinding +import com.isolaatti.home.presentation.FeedViewModel +import com.isolaatti.home.ui.FeedFragment +import com.isolaatti.posting.comments.ui.BottomSheetPostComments import com.isolaatti.posting.posts.domain.entity.Post -import com.isolaatti.posting.posts.presentation.CreatePostContract import com.isolaatti.posting.posts.presentation.EditPostContract +import com.isolaatti.posting.posts.presentation.PostListingViewModel import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter +import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity import com.isolaatti.profile.ui.ProfileActivity -import com.isolaatti.settings.ui.SettingsActivity -import com.isolaatti.utils.UrlGen import dagger.hilt.android.AndroidEntryPoint import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon @@ -54,84 +40,23 @@ import io.noties.markwon.MarkwonConfiguration import io.noties.markwon.image.coil.CoilImagesPlugin import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute import io.noties.markwon.linkify.LinkifyPlugin -import kotlinx.coroutines.launch -import org.w3c.dom.Text @AndroidEntryPoint -class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { - +class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback { companion object { - fun newInstance() = FeedFragment() - const val CALLER_ID = 20 + const val ARG_HASHTAG = "hashtag" + const val LOG_TAG = "PostListingFragment" } + var hashtag: String? = null + + private lateinit var viewBinding: FragmentPostListingBinding private val errorViewModel: ErrorMessageViewModel by activityViewModels() - private val viewModel: FeedViewModel by activityViewModels() + private val viewModel: PostListingViewModel by viewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() - private var currentUserId = 0 - - private lateinit var viewBinding: FragmentFeedBinding private lateinit var adapter: PostsRecyclerViewAdapter - private lateinit var audioPlayerConnector: AudioPlayerConnector - // region launchers - - private val createDiscussion = registerForActivityResult(CreatePostContract()) { - if(it != null) { - Toast.makeText(requireContext(), R.string.posted_successfully, Toast.LENGTH_SHORT).show() - } - } - - private val editDiscussion = registerForActivityResult(EditPostContract()) { - if(it != null) { - viewModel.onPostUpdate(it) - } - } - - // endregion - - // region observers - - private val optionsObserver: Observer = Observer { optionClicked -> - if(optionClicked?.callerId == CALLER_ID) { - // post id should come as payload - val post = optionClicked.payload as? Post ?: return@Observer - when(optionClicked.optionId) { - Options.Option.OPTION_DELETE -> { - Dialogs.buildDeletePostDialog(requireContext()) { delete -> - optionsViewModel.handle() - if(delete) { - viewModel.deletePost(post.id) - } - }.show() - - } - Options.Option.OPTION_EDIT -> { - optionsViewModel.handle() - editDiscussion.launch(post.id) - } - Options.Option.OPTION_REPORT -> { - optionsViewModel.handle() - } - } - } - } - - // endregion - - // region lifecycle - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - viewBinding = FragmentFeedBinding.inflate(inflater) - - return viewBinding.root - } - - // endregion - private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener { override fun onPlaying(isPlaying: Boolean, audio: Playable) { if(audio is Audio) @@ -160,27 +85,24 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { } - // region events + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(LOG_TAG, "onCreate ; hashtag: $hashtag") + hashtag = arguments?.getString(ARG_HASHTAG) + viewModel.getFeed(false, hashtag) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + viewBinding = FragmentPostListingBinding.inflate(inflater, container, false) + return viewBinding.root + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewBinding.topAppBar.setNavigationOnClickListener { - viewBinding.drawerLayout?.openDrawer(viewBinding.homeDrawer) - } - - - viewBinding.homeDrawer.setNavigationItemSelectedListener { - when(it.itemId) { - R.id.settings_menu_item -> { - startActivity(Intent(requireActivity(), SettingsActivity::class.java)) - true - } - R.id.about_menu_item -> { - startActivity(Intent(requireActivity(), AboutActivity::class.java)) - true - } - else -> {true} - } - } audioPlayerConnector = AudioPlayerConnector(requireContext()) audioPlayerConnector.addListener(audioPlayerConnectorListener) @@ -190,11 +112,12 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { .usePlugin(object: AbstractMarkwonPlugin() { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { builder - .imageDestinationProcessor(ImageDestinationProcessorRelativeToAbsolute + .imageDestinationProcessor( + ImageDestinationProcessorRelativeToAbsolute .create(BuildConfig.backend)) } }) - .usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader)) + .usePlugin(CoilImagesPlugin.create(requireContext(), CoilImageLoader.imageLoader)) .usePlugin(LinkifyPlugin.create()) .build() adapter = PostsRecyclerViewAdapter(markwon, this) @@ -204,44 +127,9 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { viewBinding.swipeToRefresh.setOnRefreshListener { - viewModel.getFeed(refresh = true) + viewModel.getFeed(refresh = true, hashtag) } - - viewBinding.topAppBar.setOnMenuItemClickListener { - when(it.itemId) { - R.id.menu_item_new_discussion -> { - createDiscussion.launch(Unit) - true - } - else -> { - false - } - } - } - - viewModel.userProfile.observe(viewLifecycleOwner) { - val header = viewBinding.homeDrawer.getHeaderView(0) as? ConstraintLayout - - val image: ImageView? = header?.findViewById(R.id.profileImageView) - val textViewName: TextView? = header?.findViewById(R.id.textViewName) - val textViewEmail: TextView? = header?.findViewById(R.id.textViewEmail) - val textViewUsername: TextView? = header?.findViewById(R.id.textViewUsername) - - image?.load(UrlGen.userProfileImage(it.userId), imageLoader) - - val card: MaterialCardView? = header?.findViewById(R.id.drawer_header_card) - card?.setOnClickListener { - ProfileActivity.startActivity(requireContext(), currentUserId) - } - - textViewName?.text = it.name - textViewEmail?.text = it.email - textViewUsername?.text = "@${it.uniqueUsername}" - currentUserId = it.userId - } - - viewModel.posts.observe(viewLifecycleOwner){ if (it?.first != null) { adapter.updateList(it.first!!, it.second) @@ -261,18 +149,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { errorViewModel.error.postValue(it) } - viewModel.getProfile() - optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) - - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - errorViewModel.retry.collect { - viewModel.retry() - errorViewModel.handleRetry() - } - } - } } override fun onLiked(postId: Long) = viewModel.likePost(postId) @@ -280,7 +157,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId) override fun onOptions(post: Ownable) { - optionsViewModel.setOptions(Options.POST_OPTIONS, CALLER_ID, post) + optionsViewModel.setOptions(Options.POST_OPTIONS, FeedFragment.CALLER_ID, post) val modalBottomSheet = BottomSheetPostOptionsFragment() modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG) } @@ -303,8 +180,39 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { } override fun onLoadMore() { - viewModel.getFeed(false) + viewModel.getFeed(false, hashtag) } - // endregion + private val editDiscussion = registerForActivityResult(EditPostContract()) { + if(it != null) { + viewModel.onPostUpdate(it) + } + } + + private val optionsObserver: Observer = Observer { optionClicked -> + if(optionClicked?.callerId == FeedFragment.CALLER_ID) { + // post id should come as payload + val post = optionClicked.payload as? Post ?: return@Observer + when(optionClicked.optionId) { + Options.Option.OPTION_DELETE -> { + Dialogs.buildDeletePostDialog(requireContext()) { delete -> + optionsViewModel.handle() + if(delete) { + viewModel.deletePost(post.id) + } + }.show() + + } + Options.Option.OPTION_EDIT -> { + optionsViewModel.handle() + editDiscussion.launch(post.id) + } + Options.Option.OPTION_REPORT -> { + optionsViewModel.handle() + } + } + } + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt index 1fcedbd..9adc5f9 100644 --- a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt +++ b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt @@ -111,7 +111,7 @@ class ProfileViewModel @Inject constructor( } } - override fun getFeed(refresh: Boolean) { + override fun getFeed(refresh: Boolean, hashtag: String?) { viewModelScope.launch { if(refresh) { posts.value = Pair(null, UpdateEvent(UpdateEvent.UpdateType.REFRESH, null)) @@ -132,7 +132,7 @@ class ProfileViewModel @Inject constructor( is Resource.Error -> { errorLoading.postValue(feedDtoResource.errorType) toRetry.add { - getFeed(refresh) + getFeed(refresh, hashtag) } } } diff --git a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt index a6a614d..c827eb9 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt @@ -358,7 +358,7 @@ class ProfileMainFragment : Fragment() { viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) viewBinding.swipeToRefresh.setOnRefreshListener { - viewModel.getFeed(true) + viewModel.getFeed(true, null) } viewBinding.playButton.setOnClickListener { audioDescriptionAudio?.let { audio -> @@ -396,7 +396,7 @@ class ProfileMainFragment : Fragment() { userId?.let { profileId -> viewModel.profileId = profileId viewModel.getProfile() - viewModel.getFeed(true) + viewModel.getFeed(true, null) } } @@ -466,7 +466,7 @@ class ProfileMainFragment : Fragment() { } override fun onLoadMore() { - viewModel.getFeed(false) + viewModel.getFeed(false, null) } } } diff --git a/app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt b/app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt index 669e458..37f8590 100644 --- a/app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt +++ b/app/src/main/java/com/isolaatti/search/presentation/SearchResultsAdapter.kt @@ -50,6 +50,8 @@ class SearchResultsAdapter( } } + SearchResultType.Hashtag -> R.drawable.hashtag_solid + SearchResultType.Post -> R.drawable.baseline_search_24 else -> R.drawable.baseline_search_24 } diff --git a/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt b/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt index 91ec910..9e3109e 100644 --- a/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt +++ b/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt @@ -17,7 +17,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.isolaatti.BuildConfig import com.isolaatti.R import com.isolaatti.databinding.FragmentMakeAccountBinding -import com.isolaatti.home.HomeActivity +import com.isolaatti.home.ui.HomeActivity import com.isolaatti.sign_up.domain.entity.SignUpResultCode import com.isolaatti.sign_up.presentation.MakeAccountViewModel import com.isolaatti.sign_up.presentation.SignUpViewModel diff --git a/app/src/main/res/drawable/baseline_article_24.xml b/app/src/main/res/drawable/baseline_article_24.xml new file mode 100644 index 0000000..30d5d26 --- /dev/null +++ b/app/src/main/res/drawable/baseline_article_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/hashtag_solid.xml b/app/src/main/res/drawable/hashtag_solid.xml new file mode 100644 index 0000000..9304bf8 --- /dev/null +++ b/app/src/main/res/drawable/hashtag_solid.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-land/fragment_feed.xml b/app/src/main/res/layout-land/fragment_feed.xml index 578bef4..4fc373d 100644 --- a/app/src/main/res/layout-land/fragment_feed.xml +++ b/app/src/main/res/layout-land/fragment_feed.xml @@ -17,11 +17,11 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/home_drawer" app:layout_constraintTop_toTopOf="parent" - tools:context=".home.FeedFragment"> + tools:context=".home.ui.FeedFragment"> - - - - - - + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> + tools:context=".home.ui.FeedFragment"> - - - - - + app:defaultNavHost="true" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> diff --git a/app/src/main/res/layout/fragment_post_listing.xml b/app/src/main/res/layout/fragment_post_listing.xml new file mode 100644 index 0000000..ee1e7f8 --- /dev/null +++ b/app/src/main/res/layout/fragment_post_listing.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_posts_hashtag.xml b/app/src/main/res/layout/fragment_posts_hashtag.xml index 8748d07..f9c0982 100644 --- a/app/src/main/res/layout/fragment_posts_hashtag.xml +++ b/app/src/main/res/layout/fragment_posts_hashtag.xml @@ -15,4 +15,12 @@ app:titleCentered="true"/> + + \ No newline at end of file diff --git a/app/src/main/res/navigation/home_navigation.xml b/app/src/main/res/navigation/home_navigation.xml index b6349d4..abda5dc 100644 --- a/app/src/main/res/navigation/home_navigation.xml +++ b/app/src/main/res/navigation/home_navigation.xml @@ -7,7 +7,7 @@ + android:label="HashtagPostsFragment"> diff --git a/app/src/main/res/navigation/post_listing_navigation.xml b/app/src/main/res/navigation/post_listing_navigation.xml new file mode 100644 index 0000000..703282c --- /dev/null +++ b/app/src/main/res/navigation/post_listing_navigation.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file From 94c7ec1b93e84cfb32f0bc5f1561d6564bd63ab2 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sat, 6 Apr 2024 19:56:09 -0600 Subject: [PATCH 04/10] agrego navEditor.xml para conservar posiciones de editor de navegacion --- .idea/navEditor.xml | 145 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 14 deletions(-) diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml index aab14b8..cb6a743 100644 --- a/.idea/navEditor.xml +++ b/.idea/navEditor.xml @@ -59,11 +59,66 @@ + + + + + + + + + + + + + + From 061814e7e8174adf61d33025de9be12c99a4a9c7 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sat, 6 Apr 2024 20:08:14 -0600 Subject: [PATCH 05/10] se cambia de orden los iconos de buscar y notificaciones en la barra principal --- app/src/main/res/menu/home_menu.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/menu/home_menu.xml b/app/src/main/res/menu/home_menu.xml index d01e7e1..2126d31 100644 --- a/app/src/main/res/menu/home_menu.xml +++ b/app/src/main/res/menu/home_menu.xml @@ -5,12 +5,12 @@ android:id="@+id/feedFragment" android:icon="@drawable/baseline_home_24" android:title="@string/home" /> - + \ No newline at end of file From ff1733de018962028d605873e1a6b5f32caeca8e Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sat, 6 Apr 2024 20:08:34 -0600 Subject: [PATCH 06/10] se muestra version en pantalla About --- app/src/main/java/com/isolaatti/about/AboutActivity.kt | 3 +++ app/src/main/res/layout/activity_about.xml | 10 +++++++++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/isolaatti/about/AboutActivity.kt b/app/src/main/java/com/isolaatti/about/AboutActivity.kt index 3822453..c281e5a 100644 --- a/app/src/main/java/com/isolaatti/about/AboutActivity.kt +++ b/app/src/main/java/com/isolaatti/about/AboutActivity.kt @@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.browser.customtabs.CustomTabsIntent import androidx.core.net.toUri import com.isolaatti.BuildConfig +import com.isolaatti.R import com.isolaatti.databinding.ActivityAboutBinding class AboutActivity : AppCompatActivity() { @@ -51,5 +52,7 @@ class AboutActivity : AppCompatActivity() { .build() .launchUrl(this, BuildConfig.terms.toUri()) } + + binding.appVersion.text = getString(R.string.app_version, BuildConfig.VERSION_NAME) } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index a8d36d3..2032813 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -2,7 +2,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + See all Go to hashtags Browse profiles + v%s \ No newline at end of file From 12344011bad2224544fc27ca82e53308bb11018f Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sat, 6 Apr 2024 20:08:55 -0600 Subject: [PATCH 07/10] versionCode 3; versionName 0.3-vc3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f229ea9..02127e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,8 +22,8 @@ android { applicationId "com.isolaatti" minSdk 24 targetSdk 34 - versionCode 2 - versionName "0.2" + versionCode 3 + versionName "0.3-vc3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } From e50489abf14d56df674f384bba82327a8908bb18 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sun, 7 Apr 2024 10:41:58 -0600 Subject: [PATCH 08/10] fix PostsRecyclerViewAdapter: evito llamar a onLoadMore cuando se realiza binding de elemento pero debido a actualizaciones del ultimo view --- .../posting/posts/presentation/PostsRecyclerViewAdapter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt index 445dc82..1f82a6e 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt @@ -361,7 +361,9 @@ class PostsRecyclerViewAdapter ( if(totalItems != null && totalItems > 0 && !requestedNewContent) { if(position == totalItems - 1) { requestedNewContent = true - callback.onLoadMore() + if(payloads.isEmpty()) { + callback.onLoadMore() + } } } From 133483387f9c0b077589348cf096253bf2699a1a Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sun, 7 Apr 2024 23:02:29 -0600 Subject: [PATCH 09/10] actualizacion AGP a 8.3.1 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 94c2a08..13ca289 100644 --- a/build.gradle +++ b/build.gradle @@ -13,8 +13,8 @@ buildscript { } } plugins { - id 'com.android.application' version '8.3.0' apply false - id 'com.android.library' version '8.3.0' apply false + id 'com.android.application' version '8.3.1' apply false + id 'com.android.library' version '8.3.1' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false id 'com.google.dagger.hilt.android' version '2.47' apply false id 'com.google.gms.google-services' version '4.4.0' apply false From ade2027b1727a2bb1fbd4e91a502162c2f26b2a8 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Tue, 9 Apr 2024 00:46:22 -0600 Subject: [PATCH 10/10] envio user-agent de la app en todas las peticiones hechas por OkHttp --- .../main/java/com/isolaatti/connectivity/RetrofitClient.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt b/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt index 613418d..dd66093 100644 --- a/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt +++ b/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt @@ -26,6 +26,12 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor: private val okHttpClient get() = OkHttpClient.Builder() .addInterceptor(authenticationInterceptor) + .addInterceptor { chain -> + chain.proceed(chain.request() + .newBuilder() + .header("User-Agent", "Isolaatti Android ${BuildConfig.VERSION_NAME}") + .build()) + } .connectTimeout(5, TimeUnit.MINUTES) .writeTimeout(5, TimeUnit.MINUTES) .readTimeout(5, TimeUnit.MINUTES)