From 3ef8961729f2c6e48899aacfc74ead7e120d1230 Mon Sep 17 00:00:00 2001 From: erik-everardo Date: Sat, 6 Apr 2024 19:55:25 -0600 Subject: [PATCH] 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