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" />
</intent-filter>
</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=".profile.ui.ProfileActivity"
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=".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"
android:parentActivityName=".home.HomeActivity"/>
android:parentActivityName=".home.ui.HomeActivity"/>
<activity android:name=".drafts.ui.DraftsActivity" 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"/>

View File

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

View File

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

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.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() {

View File

@ -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<Runnable> = 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<UserProfile> = MutableLiveData()
@ -88,7 +53,6 @@ class FeedViewModel @Inject constructor(
when(profile) {
is Resource.Error -> {
errorLoading.postValue(profile.errorType)
toRetry.add {
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.content.SharedPreferences
@ -45,10 +45,6 @@ class HomeActivity : IsolaattiBaseActivity() {
askNotificationPermission()
if(savedInstanceState == null) {
feedViewModel.getFeed(false)
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

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

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow
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>>>

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 }
abstract fun getFeed(refresh: Boolean)
abstract fun getFeed(refresh: Boolean, hashtag: String?)
fun likePost(postId: Long) {
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 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<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 {
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<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 {
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)
}
}
}

View File

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

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
}

View File

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

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_constraintStart_toEndOf="@+id/home_drawer"
app:layout_constraintTop_toTopOf="parent"
tools:context=".home.FeedFragment">
tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -46,26 +46,11 @@
</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:layout_width="match_parent"
android:layout_height="match_parent"
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"/>
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView

View File

@ -13,7 +13,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".home.FeedFragment">
tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
@ -36,23 +36,14 @@
</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_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<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"/>
app:defaultNavHost="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</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"/>
</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>

View File

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