This commit is contained in:
erike 2023-08-24 20:56:08 -06:00
parent 7f042917e2
commit 1e02f0bc6f
25 changed files with 167 additions and 38 deletions

View File

@ -1,8 +1,7 @@
package com.isolaatti.auth package com.isolaatti.auth
import android.content.Context
import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.auth.data.local.KeyValueDao import com.isolaatti.settings.data.KeyValueDao
import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.AuthApi
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
@ -11,7 +10,6 @@ import com.isolaatti.database.AppDatabase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
@Module @Module
@ -21,10 +19,6 @@ class Module {
fun provideAuthApi(retrofitClient: RetrofitClient): AuthApi { fun provideAuthApi(retrofitClient: RetrofitClient): AuthApi {
return retrofitClient.client.create(AuthApi::class.java) return retrofitClient.client.create(AuthApi::class.java)
} }
@Provides
fun provideKeyValueDao(database: AppDatabase): KeyValueDao {
return database.keyValueDao()
}
@Provides @Provides
fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi, keyValueDao: KeyValueDao): AuthRepository { fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi, keyValueDao: KeyValueDao): AuthRepository {

View File

@ -1,7 +1,7 @@
package com.isolaatti.auth.data package com.isolaatti.auth.data
import com.isolaatti.auth.data.local.KeyValueDao import com.isolaatti.settings.data.KeyValueDao
import com.isolaatti.auth.data.local.KeyValueEntity import com.isolaatti.settings.data.KeyValueEntity
import com.isolaatti.auth.data.remote.AuthTokenDto import com.isolaatti.auth.data.remote.AuthTokenDto
import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.AuthApi

View File

@ -0,0 +1,15 @@
package com.isolaatti.common
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.R
object Dialogs {
fun buildDeletePostDialog(context: Context, onContinue: (delete: Boolean) -> Unit, ): MaterialAlertDialogBuilder {
return MaterialAlertDialogBuilder(context)
.setTitle(R.string.delete)
.setMessage(R.string.post_will_dropped)
.setPositiveButton(R.string.yes_continue) {_, _ -> onContinue(true)}
.setNegativeButton(R.string.cancel) { _, _ -> onContinue(false)}
}
}

View File

@ -2,8 +2,8 @@ package com.isolaatti.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.isolaatti.auth.data.local.KeyValueDao import com.isolaatti.settings.data.KeyValueDao
import com.isolaatti.auth.data.local.KeyValueEntity import com.isolaatti.settings.data.KeyValueEntity
@Database(entities = [KeyValueEntity::class], version = 1) @Database(entities = [KeyValueEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {

View File

@ -15,10 +15,10 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.about.AboutActivity import com.isolaatti.about.AboutActivity
import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentFeedBinding import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.drafts.ui.DraftsActivity import com.isolaatti.drafts.ui.DraftsActivity
@ -68,14 +68,23 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
} }
val optionsObserver: Observer<OptionClicked> = Observer { val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked ->
if(it.callerId == CALLER_ID) { if(optionClicked?.callerId == CALLER_ID) {
when(it.optionId) { // post id should come as payload
val postId = optionClicked.payload as? Long ?: return@Observer
when(optionClicked.optionId) {
Options.Option.OPTION_DELETE -> { Options.Option.OPTION_DELETE -> {
Dialogs.buildDeletePostDialog(requireContext()) { delete ->
optionsViewModel.handle()
if(delete) {
viewModel.deletePost(postId)
}
}.show()
} }
Options.Option.OPTION_EDIT -> { Options.Option.OPTION_EDIT -> {
optionsViewModel.handle()
CreatePostActivity.startActivityEditMode(requireContext(), postId)
} }
Options.Option.OPTION_REPORT -> { Options.Option.OPTION_REPORT -> {
@ -197,15 +206,12 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
} }
fun onNewMenuItemClicked(v: View) {
}
override fun onLiked(postId: Long) = viewModel.likePost(postId) override fun onLiked(postId: Long) = viewModel.likePost(postId)
override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId) override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId)
override fun onOptions(postId: Long) { override fun onOptions(postId: Long) {
optionsViewModel.setOptions(Options.myPostOptions, CALLER_ID) optionsViewModel.setOptions(Options.myPostOptions, CALLER_ID, postId)
val modalBottomSheet = BottomSheetPostOptionsFragment() val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG) modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
} }

View File

@ -35,8 +35,8 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
val optionsObserver: Observer<OptionClicked> = Observer { val optionsObserver: Observer<OptionClicked?> = Observer {
if(it.callerId == CALLER_ID) { if(it?.callerId == CALLER_ID) {
optionsViewModel.optionClicked(-1) optionsViewModel.optionClicked(-1)
} }

View File

@ -1,3 +1,8 @@
package com.isolaatti.posting.common.options_bottom_sheet.domain package com.isolaatti.posting.common.options_bottom_sheet.domain
data class OptionClicked(val optionId: Int, val callerId: Int) /**
* @param optionId Identify action
* @param callerId Identify who started dialog
* @param payload Data to identify on what item perform action
*/
data class OptionClicked(val optionId: Int, val callerId: Int, val payload: Any? = null)

View File

@ -21,6 +21,7 @@ data class Options(
} }
companion object { companion object {
val noOptions = Options(0, listOf())
val myPostOptions = Options(R.string.post_options_title, listOf( val myPostOptions = Options(R.string.post_options_title, listOf(
Option(R.string.delete, R.drawable.baseline_delete_24, Option.OPTION_DELETE), Option(R.string.delete, R.drawable.baseline_delete_24, Option.OPTION_DELETE),
Option(R.string.edit, R.drawable.baseline_edit_24, Option.OPTION_EDIT), Option(R.string.edit, R.drawable.baseline_edit_24, Option.OPTION_EDIT),

View File

@ -12,16 +12,24 @@ class BottomSheetPostOptionsViewModel : ViewModel() {
private var _callerId: Int = 0 private var _callerId: Int = 0
private val _optionClicked: MutableLiveData<OptionClicked> = MutableLiveData() private val _optionClicked: MutableLiveData<OptionClicked?> = MutableLiveData()
val optionClicked: LiveData<OptionClicked> get() = _optionClicked val optionClicked: LiveData<OptionClicked?> get() = _optionClicked
fun setOptions(options: Options, callerId: Int) { private var _payload: Any? = null
fun handle() {
_optionClicked.postValue(null)
}
fun setOptions(options: Options, callerId: Int, payload: Any? = null) {
_options.postValue(options) _options.postValue(options)
_callerId = callerId _callerId = callerId
_payload = payload
} }
fun optionClicked(optionId: Int) { fun optionClicked(optionId: Int) {
_optionClicked.postValue(OptionClicked(optionId, _callerId)) _optionClicked.postValue(OptionClicked(optionId, _callerId, _payload))
} }
} }

View File

@ -1,6 +1,7 @@
package com.isolaatti.posting.common.options_bottom_sheet.ui package com.isolaatti.posting.common.options_bottom_sheet.ui
import android.os.Bundle import android.os.Bundle
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
@ -39,14 +40,16 @@ class BottomSheetPostOptionsFragment : BottomSheetDialogFragment(), OptionsRecyc
viewModel.options.observe(viewLifecycleOwner) { viewModel.options.observe(viewLifecycleOwner) {
Log.d("BottomSheetPostOptionsFragment", "entra")
renderOptions(it) renderOptions(it)
} }
viewModel.optionClicked.observe(viewLifecycleOwner) { viewModel.optionClicked.observe(viewLifecycleOwner) {
if(it.optionId > -1) { if(it != null) {
(dialog as BottomSheetDialog).dismiss() (dialog as BottomSheetDialog).dismiss()
} }
} }
} }

View File

@ -12,6 +12,6 @@ interface PostApi {
fun editPost(@Body editedPost: EditPostDto): Call<FeedDto.PostDto> fun editPost(@Body editedPost: EditPostDto): Call<FeedDto.PostDto>
@POST("Posting/Delete") @POST("Posting/Delete")
fun deletePost(@Body postToDelete: DeletePostDto): Call<Any> fun deletePost(@Body postToDelete: DeletePostDto): Call<PostDeletedDto>
} }

View File

@ -0,0 +1,3 @@
package com.isolaatti.posting.posts.data.remote
data class PostDeletedDto(val operationTime: String, val postId: Long, val success: Boolean)

View File

@ -2,11 +2,13 @@ package com.isolaatti.posting.posts.data.repository
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.EditPostDto import com.isolaatti.posting.posts.data.remote.EditPostDto
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.data.remote.FeedFilterDto import com.isolaatti.posting.posts.data.remote.FeedFilterDto
import com.isolaatti.posting.posts.data.remote.FeedsApi import com.isolaatti.posting.posts.data.remote.FeedsApi
import com.isolaatti.posting.posts.data.remote.PostApi import com.isolaatti.posting.posts.data.remote.PostApi
import com.isolaatti.posting.posts.data.remote.PostDeletedDto
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -90,4 +92,23 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }
} }
override fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>> = flow {
emit(Resource.Loading())
try {
val result = postApi.deletePost(DeletePostDto(postId)).execute()
if(result.isSuccessful) {
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
} }

View File

@ -4,6 +4,7 @@ import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.EditPostDto import com.isolaatti.posting.posts.data.remote.EditPostDto
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.data.remote.FeedFilterDto import com.isolaatti.posting.posts.data.remote.FeedFilterDto
import com.isolaatti.posting.posts.data.remote.PostDeletedDto
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -15,4 +16,5 @@ interface PostsRepository {
fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>> fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>>
fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>> fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>
fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>>
} }

View File

@ -0,0 +1,13 @@
package com.isolaatti.posting.posts.domain.use_case
import com.isolaatti.posting.posts.data.remote.PostDeletedDto
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class DeletePost @Inject constructor(private val postsRepository: PostsRepository) {
operator fun invoke(postId: Long): Flow<Resource<PostDeletedDto>> {
return postsRepository.deletePost(postId)
}
}

View File

@ -4,9 +4,9 @@ import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.likes.data.remote.LikeDto
import com.isolaatti.posting.likes.domain.repository.LikesRepository import com.isolaatti.posting.likes.domain.repository.LikesRepository
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.domain.use_case.DeletePost
import com.isolaatti.profile.domain.use_case.GetProfilePosts import com.isolaatti.profile.domain.use_case.GetProfilePosts
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -21,9 +21,12 @@ abstract class PostListingViewModelBase : ViewModel() {
lateinit var likesRepository: LikesRepository lateinit var likesRepository: LikesRepository
@Inject @Inject
lateinit var getProfilePosts: GetProfilePosts lateinit var getProfilePosts: GetProfilePosts
@Inject
lateinit var deletePostUseCase: DeletePost
val posts: MutableLiveData<Pair<FeedDto?, UpdateEvent>?> = MutableLiveData() val posts: MutableLiveData<Pair<FeedDto?, UpdateEvent>?> = MutableLiveData()
val loadingPosts = MutableLiveData(false) val loadingPosts = MutableLiveData(false)
val noMoreContent = MutableLiveData(false) val noMoreContent = MutableLiveData(false)
@ -50,7 +53,7 @@ abstract class PostListingViewModelBase : ViewModel() {
}) })
} }
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, likedPost?.post?.id))) posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, index)))
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }
@ -66,7 +69,27 @@ abstract class PostListingViewModelBase : ViewModel() {
numberOfLikes = likeDto.likesCount numberOfLikes = likeDto.likesCount
}) })
} }
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, likedPost?.post?.id))) posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, index)))
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun deletePost(postId: Long) {
viewModelScope.launch {
deletePostUseCase(postId).onEach { res ->
when(res) {
is Resource.Success -> {
val postDeleted = posts.value?.first?.data?.find { postDto -> postDto.post.id == postId }
?: return@onEach
val index = posts.value?.first?.data?.indexOf(postDeleted)
posts.value?.first?.data?.removeAt(index!!)
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_REMOVED, index)))
}
is Resource.Loading -> {}
is Resource.Error -> {}
}
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }

View File

@ -136,8 +136,8 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
notifyDataSetChanged() notifyDataSetChanged()
return return
} }
val postUpdated = feedDto?.data?.find { p -> p.post.id == updateEvent.affectedId } val postUpdated = updateEvent.affectedPosition?.let { feedDto?.data?.get(it) }
val position = feedDto?.data?.indexOf(postUpdated) val position = updateEvent.affectedPosition
previousSize = itemCount previousSize = itemCount
feedDto = updatedFeed feedDto = updatedFeed

View File

@ -1,6 +1,6 @@
package com.isolaatti.posting.posts.presentation package com.isolaatti.posting.posts.presentation
data class UpdateEvent(val updateType: UpdateType, val affectedId: Long?) { data class UpdateEvent(val updateType: UpdateType, val affectedPosition: Int?) {
enum class UpdateType { enum class UpdateType {
POST_LIKED, POST_LIKED,
POST_COMMENTED, POST_COMMENTED,

View File

@ -1,6 +1,7 @@
package com.isolaatti.posting.posts.ui package com.isolaatti.posting.posts.ui
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle import android.os.PersistableBundle
@ -31,6 +32,14 @@ class CreatePostActivity : IsolaattiBaseActivity() {
const val EXTRA_KEY_POST_ID = "postId" const val EXTRA_KEY_POST_ID = "postId"
const val EXTRA_KEY_POST_POSTED = "post" const val EXTRA_KEY_POST_POSTED = "post"
fun startActivityEditMode(context: Context, postId: Long) {
val intent = Intent(context, CreatePostActivity::class.java).apply {
putExtra(EXTRA_KEY_MODE, EXTRA_MODE_EDIT)
putExtra(EXTRA_KEY_POST_ID, postId)
}
context.startActivity(intent)
}
} }
lateinit var binding: ActivityCreatePostBinding lateinit var binding: ActivityCreatePostBinding

View File

@ -211,7 +211,7 @@ class ProfileMainFragment : Fragment() {
} }
override fun onOptions(postId: Long) { override fun onOptions(postId: Long) {
optionsViewModel.setOptions(Options.myPostOptions, FeedFragment.CALLER_ID) optionsViewModel.setOptions(Options.myPostOptions, FeedFragment.CALLER_ID, postId)
val modalBottomSheet = BottomSheetPostOptionsFragment() val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG) modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
} }

View File

@ -0,0 +1,6 @@
package com.isolaatti.reports.ui
import androidx.appcompat.app.AppCompatActivity
class ReportsActivity : AppCompatActivity() {
}

View File

@ -0,0 +1,17 @@
package com.isolaatti.settings
import com.isolaatti.database.AppDatabase
import com.isolaatti.settings.data.KeyValueDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
fun provideKeyValueDao(database: AppDatabase): KeyValueDao {
return database.keyValueDao()
}
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.auth.data.local package com.isolaatti.settings.data
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert

View File

@ -1,4 +1,4 @@
package com.isolaatti.auth.data.local package com.isolaatti.settings.data
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey

View File

@ -65,4 +65,7 @@
<string name="million_suffix">M</string> <string name="million_suffix">M</string>
<string name="people">People</string> <string name="people">People</string>
<string name="about">About</string> <string name="about">About</string>
<string name="post_will_dropped">This discussion and all related content will be dropped. Continue?</string>
<string name="yes_continue">Yes, delete</string>
<string name="cancel">Cancel</string>
</resources> </resources>