busqueda casi completa, refactorizacion, iconos nuevos en resultados, posts de hashtag

This commit is contained in:
erik-everardo 2024-04-06 19:55:25 -06:00
parent 000937ded4
commit 3ef8961729
27 changed files with 390 additions and 275 deletions

View File

@ -26,15 +26,15 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".home.HomeActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".home.ui.HomeActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".profile.ui.ProfileActivity" <activity android:name=".profile.ui.ProfileActivity"
android:theme="@style/Theme.Isolaatti" android:theme="@style/Theme.Isolaatti"
android:parentActivityName=".home.HomeActivity"/> android:parentActivityName=".home.ui.HomeActivity"/>
<activity android:name=".settings.ui.SettingsActivity" android:theme="@style/Theme.Isolaatti"/> <activity android:name=".settings.ui.SettingsActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".posting.posts.ui.CreatePostActivity" android:theme="@style/Theme.Isolaatti" android:windowSoftInputMode="adjustResize"/> <activity android:name=".posting.posts.ui.CreatePostActivity" android:theme="@style/Theme.Isolaatti" android:windowSoftInputMode="adjustResize"/>
<activity android:name=".posting.posts.viewer.ui.PostViewerActivity" android:theme="@style/Theme.Isolaatti" <activity android:name=".posting.posts.viewer.ui.PostViewerActivity" android:theme="@style/Theme.Isolaatti"
android:parentActivityName=".home.HomeActivity"/> android:parentActivityName=".home.ui.HomeActivity"/>
<activity android:name=".drafts.ui.DraftsActivity" android:theme="@style/Theme.Isolaatti"/> <activity android:name=".drafts.ui.DraftsActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".about.AboutActivity" android:theme="@style/Theme.Isolaatti"/> <activity android:name=".about.AboutActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".images.picture_viewer.ui.PictureViewerActivity" android:theme="@style/Theme.Isolaatti"/> <activity android:name=".images.picture_viewer.ui.PictureViewerActivity" android:theme="@style/Theme.Isolaatti"/>

View File

@ -7,7 +7,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.home.HomeActivity import com.isolaatti.home.ui.HomeActivity
import com.isolaatti.login.LogInActivity import com.isolaatti.login.LogInActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject

View File

@ -6,7 +6,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.connectivity.ConnectivityCallbackImpl
import com.isolaatti.connectivity.NetworkStatus import com.isolaatti.connectivity.NetworkStatus
import com.isolaatti.home.HomeActivity
import com.isolaatti.login.LogInActivity import com.isolaatti.login.LogInActivity
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint

View File

@ -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 {
}

View File

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

View File

@ -5,9 +5,12 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.isolaatti.databinding.FragmentPostsHashtagBinding import com.isolaatti.databinding.FragmentPostsHashtagBinding
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.isolaatti.R
import com.isolaatti.posting.posts.ui.PostListingFragment
class HashtagPostsFragment : Fragment() { class HashtagPostsFragment : Fragment() {
@ -29,6 +32,10 @@ class HashtagPostsFragment : Fragment() {
binding.toolbar.title = "#${navArgs.hashtag}" binding.toolbar.title = "#${navArgs.hashtag}"
setupListeners() 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() { private fun setupListeners() {

View File

@ -2,12 +2,12 @@ package com.isolaatti.home.presentation
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
import com.isolaatti.posting.posts.presentation.UpdateEvent 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.entity.UserProfile
import com.isolaatti.profile.domain.use_case.GetProfile import com.isolaatti.profile.domain.use_case.GetProfile
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
@ -22,9 +22,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FeedViewModel @Inject constructor( class FeedViewModel @Inject constructor(
private val getProfileUseCase: GetProfile, private val getProfileUseCase: GetProfile,
private val authRepository: AuthRepository, private val authRepository: AuthRepository
private val postsRepository: PostsRepository ) : ViewModel() {
) : PostListingViewModelBase() {
private val toRetry: MutableList<Runnable> = mutableListOf() private val toRetry: MutableList<Runnable> = mutableListOf()
@ -39,41 +38,7 @@ class FeedViewModel @Inject constructor(
toRetry.clear() 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 // User profile
private val _userProfile: MutableLiveData<UserProfile> = MutableLiveData() private val _userProfile: MutableLiveData<UserProfile> = MutableLiveData()
@ -88,7 +53,6 @@ class FeedViewModel @Inject constructor(
when(profile) { when(profile) {
is Resource.Error -> { is Resource.Error -> {
errorLoading.postValue(profile.errorType)
toRetry.add { toRetry.add {
getProfile() getProfile()
} }

View File

@ -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()
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.home package com.isolaatti.home.ui
import android.Manifest import android.Manifest
import android.content.SharedPreferences import android.content.SharedPreferences
@ -45,10 +45,6 @@ class HomeActivity : IsolaattiBaseActivity() {
askNotificationPermission() askNotificationPermission()
if(savedInstanceState == null) {
feedViewModel.getFeed(false)
}
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@ -1,5 +1,6 @@
package com.isolaatti.posting.posts.data.remote package com.isolaatti.posting.posts.data.remote
import com.isolaatti.common.ResultDto
import com.isolaatti.profile.data.remote.ProfileListItemDto import com.isolaatti.profile.data.remote.ProfileListItemDto
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
@ -22,4 +23,7 @@ interface FeedsApi {
@GET("Feed") @GET("Feed")
fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto> fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto>
@GET("hashtags/hashtag/{hashtag}")
fun getHashtagPosts(@Path("hashtag") hashtag: String, @Query("after") afterPost: Long? = null): Call<FeedDto>
} }

View File

@ -1,5 +1,6 @@
package com.isolaatti.posting.posts.data.repository package com.isolaatti.posting.posts.data.repository
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.isolaatti.posting.posts.data.remote.CreatePostDto import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.DeletePostDto import com.isolaatti.posting.posts.data.remote.DeletePostDto
@ -18,10 +19,18 @@ import retrofit2.awaitResponse
import javax.inject.Inject import javax.inject.Inject
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository { class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository {
override fun getFeed(lastId: Long): Flow<Resource<MutableList<Post>>> = flow { companion object {
const val LOG_TAG = "PostsRepositoryImpl"
}
override fun getFeed(lastId: Long, hashtag: String?): Flow<Resource<MutableList<Post>>> = flow {
emit(Resource.Loading()) emit(Resource.Loading())
try { 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) { if(result.isSuccessful) {
emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) })) emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) }))
return@flow return@flow
@ -32,7 +41,8 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError)) 500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError)) 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)) emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }
} }

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow
interface PostsRepository { interface PostsRepository {
fun getFeed(lastId: Long): Flow<Resource<MutableList<Post>>> fun getFeed(lastId: Long, hashtag: String?): Flow<Resource<MutableList<Post>>>
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<MutableList<Post>>> fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<MutableList<Post>>>

View File

@ -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)
}
}
}

View File

@ -44,7 +44,7 @@ abstract class PostListingViewModelBase : ViewModel() {
fun getLastId(): Long = try { posts.value?.first?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 } 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) { fun likePost(postId: Long) {
viewModelScope.launch { viewModelScope.launch {

View File

@ -1,52 +1,38 @@
package com.isolaatti.home package com.isolaatti.posting.posts.ui
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import androidx.fragment.app.Fragment
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager 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.BuildConfig
import com.isolaatti.R
import com.isolaatti.about.AboutActivity
import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.audio.common.domain.Playable import com.isolaatti.audio.common.domain.Playable
import com.isolaatti.audio.player.AudioPlayerConnector 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.Dialogs
import com.isolaatti.common.ErrorMessageViewModel 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.OnUserInteractedWithPostCallback
import com.isolaatti.common.Ownable import com.isolaatti.common.Ownable
import com.isolaatti.common.options_bottom_sheet.domain.OptionClicked 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.domain.Options
import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment 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.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.presentation.EditPostContract 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.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.ui.ProfileActivity import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.UrlGen
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon 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.coil.CoilImagesPlugin
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.launch
import org.w3c.dom.Text
@AndroidEntryPoint @AndroidEntryPoint
class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
companion object { companion object {
fun newInstance() = FeedFragment() const val ARG_HASHTAG = "hashtag"
const val CALLER_ID = 20 const val LOG_TAG = "PostListingFragment"
} }
var hashtag: String? = null
private lateinit var viewBinding: FragmentPostListingBinding
private val errorViewModel: ErrorMessageViewModel by activityViewModels() private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val viewModel: FeedViewModel by activityViewModels() private val viewModel: PostListingViewModel by viewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private var currentUserId = 0
private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: PostsRecyclerViewAdapter private lateinit var adapter: PostsRecyclerViewAdapter
private lateinit var audioPlayerConnector: AudioPlayerConnector 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<OptionClicked?> = 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 { private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
override fun onPlaying(isPlaying: Boolean, audio: Playable) { override fun onPlaying(isPlaying: Boolean, audio: Playable) {
if(audio is Audio) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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 = AudioPlayerConnector(requireContext())
audioPlayerConnector.addListener(audioPlayerConnectorListener) audioPlayerConnector.addListener(audioPlayerConnectorListener)
@ -190,11 +112,12 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
.usePlugin(object: AbstractMarkwonPlugin() { .usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder builder
.imageDestinationProcessor(ImageDestinationProcessorRelativeToAbsolute .imageDestinationProcessor(
ImageDestinationProcessorRelativeToAbsolute
.create(BuildConfig.backend)) .create(BuildConfig.backend))
} }
}) })
.usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader)) .usePlugin(CoilImagesPlugin.create(requireContext(), CoilImageLoader.imageLoader))
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()
adapter = PostsRecyclerViewAdapter(markwon, this) adapter = PostsRecyclerViewAdapter(markwon, this)
@ -204,44 +127,9 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
viewBinding.swipeToRefresh.setOnRefreshListener { 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){ viewModel.posts.observe(viewLifecycleOwner){
if (it?.first != null) { if (it?.first != null) {
adapter.updateList(it.first!!, it.second) adapter.updateList(it.first!!, it.second)
@ -261,18 +149,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
errorViewModel.error.postValue(it) errorViewModel.error.postValue(it)
} }
viewModel.getProfile()
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) 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) 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 onUnLiked(postId: Long) = viewModel.unLikePost(postId)
override fun onOptions(post: Ownable) { 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() val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG) modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
} }
@ -303,8 +180,39 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
override fun onLoadMore() { 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<OptionClicked?> = 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()
}
}
}
}
} }

View File

@ -111,7 +111,7 @@ class ProfileViewModel @Inject constructor(
} }
} }
override fun getFeed(refresh: Boolean) { override fun getFeed(refresh: Boolean, hashtag: String?) {
viewModelScope.launch { viewModelScope.launch {
if(refresh) { if(refresh) {
posts.value = Pair(null, UpdateEvent(UpdateEvent.UpdateType.REFRESH, null)) posts.value = Pair(null, UpdateEvent(UpdateEvent.UpdateType.REFRESH, null))
@ -132,7 +132,7 @@ class ProfileViewModel @Inject constructor(
is Resource.Error -> { is Resource.Error -> {
errorLoading.postValue(feedDtoResource.errorType) errorLoading.postValue(feedDtoResource.errorType)
toRetry.add { toRetry.add {
getFeed(refresh) getFeed(refresh, hashtag)
} }
} }
} }

View File

@ -358,7 +358,7 @@ class ProfileMainFragment : Fragment() {
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewBinding.swipeToRefresh.setOnRefreshListener { viewBinding.swipeToRefresh.setOnRefreshListener {
viewModel.getFeed(true) viewModel.getFeed(true, null)
} }
viewBinding.playButton.setOnClickListener { viewBinding.playButton.setOnClickListener {
audioDescriptionAudio?.let { audio -> audioDescriptionAudio?.let { audio ->
@ -396,7 +396,7 @@ class ProfileMainFragment : Fragment() {
userId?.let { profileId -> userId?.let { profileId ->
viewModel.profileId = profileId viewModel.profileId = profileId
viewModel.getProfile() viewModel.getProfile()
viewModel.getFeed(true) viewModel.getFeed(true, null)
} }
} }
@ -466,7 +466,7 @@ class ProfileMainFragment : Fragment() {
} }
override fun onLoadMore() { override fun onLoadMore() {
viewModel.getFeed(false) viewModel.getFeed(false, null)
} }
} }
} }

View File

@ -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 else -> R.drawable.baseline_search_24
} }

View File

@ -17,7 +17,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentMakeAccountBinding 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.domain.entity.SignUpResultCode
import com.isolaatti.sign_up.presentation.MakeAccountViewModel import com.isolaatti.sign_up.presentation.MakeAccountViewModel
import com.isolaatti.sign_up.presentation.SignUpViewModel import com.isolaatti.sign_up.presentation.SignUpViewModel

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="448dp"
android:height="512dp"
android:viewportWidth="448"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M181.3,32.4c17.4,2.9 29.2,19.4 26.3,36.8L197.8,128h95.1l11.5,-69.3c2.9,-17.4 19.4,-29.2 36.8,-26.3s29.2,19.4 26.3,36.8L357.8,128H416c17.7,0 32,14.3 32,32s-14.3,32 -32,32H347.1L325.8,320H384c17.7,0 32,14.3 32,32s-14.3,32 -32,32H315.1l-11.5,69.3c-2.9,17.4 -19.4,29.2 -36.8,26.3s-29.2,-19.4 -26.3,-36.8l9.8,-58.7H155.1l-11.5,69.3c-2.9,17.4 -19.4,29.2 -36.8,26.3s-29.2,-19.4 -26.3,-36.8L90.2,384H32c-17.7,0 -32,-14.3 -32,-32s14.3,-32 32,-32h68.9l21.3,-128H64c-17.7,0 -32,-14.3 -32,-32s14.3,-32 32,-32h68.9l11.5,-69.3c2.9,-17.4 19.4,-29.2 36.8,-26.3zM187.1,192L165.8,320h95.1l21.3,-128H187.1z"/>
</vector>

View File

@ -17,11 +17,11 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_drawer" app:layout_constraintStart_toEndOf="@+id/home_drawer"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:context=".home.FeedFragment"> tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout" android:id="@+id/topAppBar_layout"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -46,26 +46,11 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/swipe_to_refresh" android:id="@+id/post_list_fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topAppBar_layout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="bottom"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView <com.google.android.material.navigation.NavigationView

View File

@ -13,7 +13,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".home.FeedFragment"> tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout" android:id="@+id/topAppBar_layout"
@ -36,23 +36,14 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_to_refresh" <androidx.fragment.app.FragmentContainerView
android:id="@+id/post_list_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> app:defaultNavHost="true"
<androidx.recyclerview.widget.RecyclerView app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="bottom"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_to_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,4 +15,12 @@
app:titleCentered="true"/> app:titleCentered="true"/>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/post_list_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -7,7 +7,7 @@
<fragment <fragment
android:id="@+id/feedFragment" android:id="@+id/feedFragment"
android:name="com.isolaatti.home.FeedFragment" android:name="com.isolaatti.home.ui.FeedFragment"
android:label="fragment_feed" android:label="fragment_feed"
tools:layout="@layout/fragment_feed" > tools:layout="@layout/fragment_feed" >
<action <action

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/post_listing_navigation"
app:startDestination="@id/postListingFragment">
<fragment
android:id="@+id/postListingFragment"
android:name="com.isolaatti.posting.posts.ui.PostListingFragment"
android:label="PostListingFragment" >
<argument
android:name="hashtag"
app:argType="string"
app:nullable="true" />
</fragment>
</navigation>