This commit is contained in:
erik-everardo 2025-01-18 11:57:46 -06:00
parent 727007d94f
commit 571b893fcd
18 changed files with 183 additions and 129 deletions

View File

@ -1,9 +1,8 @@
package com.isolaatti.images.common.data.remote
data class ImageDto(
val id: String,
val userId: Int,
val squadId: String?,
val username: String,
val idOnFirebase: String
val squadId: String?
)

View File

@ -34,7 +34,7 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
val response = imagesApi.getImagesOfUser(userId, lastId).awaitResponse()
if(response.isSuccessful) {
val imagesDto = response.body()
val images = imagesDto?.data?.map { Image.fromDto(it) }
val images = imagesDto?.data?.map { Image(it.id) }
emit(Resource.Success(images))
@ -93,7 +93,7 @@ class ImagesRepositoryImpl @Inject constructor(private val imagesApi: ImagesApi,
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
return@flow
}
val image = Image.fromDto(imageDto)
val image = Image(imageDto.id)
emit(Resource.Success(image))
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))

View File

@ -1,5 +1,7 @@
package com.isolaatti.images.common.domain.entity
import android.os.Parcel
import android.os.Parcelable
import com.isolaatti.common.Deletable
import com.isolaatti.images.common.data.remote.ImageDto
import com.isolaatti.markdown.Generators
@ -7,19 +9,16 @@ import com.isolaatti.utils.UrlGen
import java.io.Serializable
data class Image(
val id: String,
val userId: Int,
val username: String
): Deletable(), Serializable {
val id: String
): Deletable(), Parcelable {
val imageUrl: String get() = UrlGen.imageUrl(id)
val smallImageUrl : String get() = UrlGen.imageUrl(id, UrlGen.IMAGE_MODE_SMALL)
val reducedImageUrl: String get() = UrlGen.imageUrl(id, UrlGen.IMAGE_MODE_REDUCED)
val markdown: String get() = Generators.generateImage(imageUrl)
companion object {
fun fromDto(imageDto: ImageDto) = Image(imageDto.id, imageDto.userId, imageDto.username)
}
constructor(parcel: Parcel) : this(parcel.readString()!!)
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -28,16 +27,30 @@ data class Image(
other as Image
if (id != other.id) return false
if (userId != other.userId) return false
if (username != other.username) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + userId
result = 31 * result + username.hashCode()
return result
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(id)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Image> {
override fun createFromParcel(parcel: Parcel): Image {
return Image(parcel)
}
override fun newArray(size: Int): Array<Image?> {
return arrayOfNulls(size)
}
}
}

View File

@ -53,7 +53,7 @@ class PictureViewerImageWrapperFragment : Fragment() {
fun getInstance(image: Image): PictureViewerImageWrapperFragment {
val fragment = PictureViewerImageWrapperFragment()
fragment.arguments = Bundle().apply {
putSerializable(ARGUMENT_IMAGE, image)
putParcelable(ARGUMENT_IMAGE, image)
}
return fragment

View File

@ -19,7 +19,7 @@ class PictureViewerMainFragment : Fragment() {
private val onPageChangeCallback = object: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
binding.imageAuthor.text = images[position].username
//binding.imageAuthor.text = images[position].username
}
}
@ -42,7 +42,7 @@ class PictureViewerMainFragment : Fragment() {
binding.viewpager.adapter = adapter
binding.viewpager.setCurrentItem(position, false)
binding.viewpager.registerOnPageChangeCallback(onPageChangeCallback)
binding.imageAuthor.text = images[position].username
//binding.imageAuthor.text = images[position].username
}
override fun onDestroyView() {

View File

@ -1,9 +1,7 @@
package com.isolaatti.posting.posts.data.remote
import android.os.Parcel
import android.os.Parcelable
import com.isolaatti.audio.common.data.AudioDto
import java.io.Serializable
import com.isolaatti.images.common.data.remote.ImageDto
data class FeedDto(
val data: MutableList<PostDto>,
@ -18,6 +16,7 @@ data class FeedDto(
return this
}
data class PostDto(
val post: Post,
var numberOfLikes: Int,
@ -26,17 +25,9 @@ data class FeedDto(
val squadName: String?,
var liked: Boolean,
var audio: AudioDto?
): Parcelable {
){
constructor(parcel: Parcel) : this(
parcel.readParcelable(Post::class.java.classLoader)!!,
parcel.readInt(),
parcel.readInt(),
parcel.readString()!!,
parcel.readString(),
parcel.readByte() != 0.toByte(),
parcel.readParcelable(AudioDto::class.java.classLoader)
)
val list: MutableList<ImageDto> = mutableListOf()
data class Post(
val id: Long,
@ -47,70 +38,9 @@ data class FeedDto(
var audioId: String?,
val squadId: String?,
val linkedDiscussionId: Long,
val linkedCommentId: Long
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readLong(),
parcel.readString() ?: "",
parcel.readInt(),
parcel.readInt(),
parcel.readString() ?: "",
parcel.readString(),
parcel.readString(),
parcel.readLong(),
parcel.readLong()
) {
}
val linkedCommentId: Long,
var images: List<ImageDto>
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeString(textContent)
parcel.writeInt(userId)
parcel.writeInt(privacy)
parcel.writeString(date)
parcel.writeString(audioId)
parcel.writeString(squadId)
parcel.writeLong(linkedDiscussionId)
parcel.writeLong(linkedCommentId)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Post> {
override fun createFromParcel(parcel: Parcel): Post {
return Post(parcel)
}
override fun newArray(size: Int): Array<Post?> {
return arrayOfNulls(size)
}
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(post, flags)
parcel.writeInt(numberOfLikes)
parcel.writeInt(numberOfComments)
parcel.writeString(userName)
parcel.writeString(squadName)
parcel.writeByte(if (liked) 1 else 0)
parcel.writeSerializable(audio)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<PostDto> {
override fun createFromParcel(parcel: Parcel): PostDto {
return PostDto(parcel)
}
override fun newArray(size: Int): Array<PostDto?> {
return arrayOfNulls(size)
}
}
}
}

View File

@ -7,10 +7,8 @@ import android.net.Uri
import android.os.Build
import android.util.Log
import com.google.gson.Gson
import com.isolaatti.images.common.data.remote.DeleteImagesDto
import com.isolaatti.images.common.data.remote.ImageDto
import com.isolaatti.images.common.data.remote.ImagesApi
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.DeletePostDto
import com.isolaatti.posting.posts.data.remote.EditPostDto
@ -20,6 +18,7 @@ import com.isolaatti.posting.posts.data.remote.FeedsApi
import com.isolaatti.posting.posts.data.remote.PostApi
import com.isolaatti.posting.posts.data.remote.PostDeletedDto
import com.isolaatti.posting.posts.data.remote.VersionDto
import com.isolaatti.posting.posts.domain.PostingSteps
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.profile.domain.entity.ProfileListItem
@ -82,7 +81,7 @@ class PostsRepositoryImpl @Inject constructor(
}
}
override fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<FeedDto.PostDto>> = flow {
override fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<PostingSteps>> = flow {
emit(Resource.Loading())
try {
val hasImages = images.isNotEmpty()
@ -103,6 +102,7 @@ class PostsRepositoryImpl @Inject constructor(
emit(Resource.Error())
return@flow
}
emit(Resource.Success(PostingSteps.UploadingPhotos))
images.forEach { imageUri ->
var imageInputStream: InputStream? = null
try {
@ -146,20 +146,19 @@ class PostsRepositoryImpl @Inject constructor(
postApi.deletePost(DeletePostDto(postId)).awaitResponse()
emit(Resource.Error(Resource.Error.ErrorType.ServerError, "Some images were not processed correctly"))
} else {
postApi.setIsDraft(postId, false)
val response = postApi.getPost(postId).awaitResponse()
val setIsDraftCall = postApi.setIsDraft(postId, false).awaitResponse()
if(response.isSuccessful) {
emit(Resource.Success(response.body()))
if(setIsDraftCall.isSuccessful) {
emit(Resource.Success(PostingSteps.Finished))
} else {
val msg = "Post posted but could not retrieve updated post data from server"
Log.e(LOG_TAG, msg)
emit(Resource.Error(Resource.Error.mapErrorCode(response.code()), msg))
val code = setIsDraftCall.code()
Log.e(LOG_TAG, "Could not set as \"not draft\"")
emit(Resource.Error(Resource.Error.mapErrorCode(code), "Could not set as \"not draft\""))
}
}
} else {
emit(Resource.Success(result.body()))
emit(Resource.Success(PostingSteps.Finished))
return@flow
}

View File

@ -0,0 +1,5 @@
package com.isolaatti.posting.posts.domain
enum class PostingSteps {
PostContent, UploadingPhotos, Finished, Unspecified
}

View File

@ -18,7 +18,7 @@ interface PostsRepository {
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<MutableList<Post>>>
fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<FeedDto.PostDto>>
fun makePost(createPostDto: CreatePostDto, images: List<Uri>): Flow<Resource<PostingSteps>>
fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>
fun deletePost(postId: Long): Flow<Resource<PostDeletedDto>>
fun loadPost(postId: Long): Flow<Resource<Post>>

View File

@ -5,6 +5,7 @@ import android.os.Parcelable
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.common.Ownable
import com.isolaatti.common.hashtagRegex
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.posting.posts.data.remote.FeedDto
data class Post(
@ -20,7 +21,8 @@ data class Post(
val userName: String,
val squadName: String?,
var liked: Boolean,
val audio: Audio? = null
val audio: Audio? = null,
val images: List<Image>
) : Ownable, Parcelable {
constructor(parcel: Parcel) : this(
parcel.readLong(),
@ -35,7 +37,8 @@ data class Post(
parcel.readString()!!,
parcel.readString(),
parcel.readByte() != 0.toByte(),
parcel.readSerializable() as? Audio
parcel.readSerializable() as? Audio,
parcel.readParcelableArray(Image::class.java.classLoader)?.toList() as? List<Image> ?: emptyList()
) {
}
@ -55,7 +58,8 @@ data class Post(
userName = it.userName,
squadName = it.squadName,
liked = it.liked,
audio = it.audio?.let { audioDto -> Audio.fromDto(audioDto) }
audio = it.audio?.let { audioDto -> Audio.fromDto(audioDto) },
images = it.post.images.map { imageDto -> Image(imageDto.id) }
)
}.toMutableList()
}
@ -73,7 +77,8 @@ data class Post(
numberOfLikes = postDto.numberOfLikes,
userName = postDto.userName,
squadName = postDto.squadName,
liked = postDto.liked
liked = postDto.liked,
images = postDto.post.images.map { imageDto -> Image(imageDto.id) }
)
}
@ -103,6 +108,7 @@ data class Post(
parcel.writeString(squadName)
parcel.writeByte(if (liked) 1 else 0)
parcel.writeSerializable(audio)
parcel.writeParcelableArray(images.toTypedArray(), 0)
}
override fun describeContents(): Int {

View File

@ -2,7 +2,7 @@ package com.isolaatti.posting.posts.domain.use_case
import android.net.Uri
import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.domain.PostingSteps
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
@ -15,7 +15,7 @@ class MakePost @Inject constructor(private val postsRepository: PostsRepository)
images: List<Uri>,
audioId: String?,
squadId: String?
): Flow<Resource<FeedDto.PostDto>> {
): Flow<Resource<PostingSteps>> {
return postsRepository.makePost(CreatePostDto(privacy, content, audioId, squadId), images)
}
}

View File

@ -11,6 +11,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.Companion.PRIVACY_ISOLAATTI
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.domain.PostingSteps
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.posting.posts.domain.use_case.EditPost
import com.isolaatti.posting.posts.domain.use_case.LoadSinglePost
@ -39,7 +40,6 @@ class CreatePostViewModel @Inject constructor(
}
val validation: MutableLiveData<Boolean> = MutableLiveData(false)
val posted: MutableLiveData<Post?> = MutableLiveData()
val error: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
val sendingPost: MutableLiveData<Boolean> = MutableLiveData(false)
val postToEdit: MutableLiveData<EditPostDto> = MutableLiveData()
@ -49,6 +49,9 @@ class CreatePostViewModel @Inject constructor(
private val _photos: MutableStateFlow<List<Uri>> = MutableStateFlow(emptyList())
val photos: StateFlow<List<Uri>> get() = _photos
private val _postingStep = MutableStateFlow(PostingSteps.Unspecified)
val postingStep: StateFlow<PostingSteps> get() = _postingStep
val audioAttachment: MutableLiveData<Playable?> = MutableLiveData()
private var audioDraft: Long? = null
@ -71,8 +74,12 @@ class CreatePostViewModel @Inject constructor(
).onEach {
when(it) {
is Resource.Success -> {
sendingPost.postValue(false)
posted.postValue(Post.fromPostDto(it.data!!))
if(it.data == PostingSteps.Finished) {
sendingPost.postValue(false)
}
_postingStep.value = it.data!!
}
is Resource.Error -> {
sendingPost.postValue(false)
@ -103,7 +110,6 @@ class CreatePostViewModel @Inject constructor(
when(it) {
is Resource.Success -> {
sendingPost.postValue(false)
posted.postValue(Post.fromPostDto(it.data!!))
}
is Resource.Error -> {
sendingPost.postValue(false)

View File

@ -0,0 +1,35 @@
package com.isolaatti.posting.posts.presentation
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load
import com.isolaatti.images.common.domain.entity.Image
class PostImagesViewPagerAdapter(private val images: List<Image>) :
RecyclerView.Adapter<PostImagesViewPagerAdapter.PostImagesViewPagerAdapterItemViewHolder>() {
class PostImagesViewPagerAdapterItemViewHolder(val imageView: ImageView) : ViewHolder(imageView)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PostImagesViewPagerAdapterItemViewHolder {
return PostImagesViewPagerAdapterItemViewHolder(
imageView = ImageView(parent.context).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
)
}
override fun getItemCount(): Int = images.size
override fun onBindViewHolder(holder: PostImagesViewPagerAdapterItemViewHolder, position: Int) {
val image = images[position]
holder.imageView.load(image.reducedImageUrl)
}
}

View File

@ -12,6 +12,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.text.set
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load
@ -162,6 +163,14 @@ class PostsRecyclerViewAdapter (
itemBinding.audio.root.visibility = View.GONE
itemBinding.audio.playButton.setOnClickListener(null)
}
if(post.images.isNotEmpty()) {
itemBinding.photosViewPager.isVisible = true
itemBinding.photosViewPager.adapter = PostImagesViewPagerAdapter(post.images)
} else {
itemBinding.photosViewPager.isVisible = false
itemBinding.photosViewPager.adapter = null
}
itemBinding.shareButton.setOnClickListener {
callback.onShare(post.id)
}

View File

@ -13,14 +13,18 @@ import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -29,7 +33,9 @@ import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -40,6 +46,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.FileProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.isolaatti.MyApplication
@ -48,8 +55,10 @@ import com.isolaatti.audio.common.components.AudioRecorder
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.common.IsolaattiTheme
import com.isolaatti.posting.posts.components.PostAttachments
import com.isolaatti.posting.posts.domain.PostingSteps
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import java.io.File
import java.util.Calendar
@ -146,6 +155,33 @@ class CreatePostActivity : IsolaattiBaseActivity() {
}
) {
val scrollState = rememberScrollState()
val posting by viewModel.sendingPost.observeAsState()
if(posting == true) {
val postingStep by viewModel.postingStep.collectAsState()
AlertDialog(
onDismissRequest = {},
text = {
Row {
CircularProgressIndicator()
Text(
when(postingStep) {
PostingSteps.PostContent -> getString(R.string.posting)
PostingSteps.UploadingPhotos -> getString(R.string.uploading_photos)
PostingSteps.Finished -> ""
PostingSteps.Unspecified -> ""
}
)
}
},
dismissButton = null,
title = {
Text(getString(R.string.posting))
},
confirmButton = {}
)
}
Column(modifier = Modifier.padding(it).verticalScroll(scrollState)) {
OutlinedTextField(
value = text,

View File

@ -182,10 +182,7 @@ class ProfileMainFragment : Fragment() {
val profilePictureUrl = profile?.profilePictureUrl
if(profilePictureUrl != null) {
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(
Image(
profile.profileImageId ?: "",
profile.userId,
profile.uniqueUsername)
Image(profile.profileImageId ?: "")
))
}
}

View File

@ -15,15 +15,20 @@
android:layout_margin="8dp"
>
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp">
<RelativeLayout android:layout_width="match_parent"
<RelativeLayout
android:id="@+id/post_header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/avatar_picture"
android:layout_width="40dp"
@ -65,7 +70,17 @@
layout="@layout/audio_attachment"
android:layout_marginHorizontal="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/post_header_container"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/photos_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/audio"
app:layout_constraintDimensionRatio="H,1:1"
android:visibility="gone"
tools:visibility="visible"/>
<TextView
android:id="@+id/post_content"
@ -76,13 +91,15 @@
android:linksClickable="true"
android:textSize="16sp"
tools:text="Hola"
android:fontFamily="sans-serif"/>
android:fontFamily="sans-serif"
app:layout_constraintTop_toBottomOf="@id/photos_view_pager"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start">
android:gravity="start"
app:layout_constraintTop_toBottomOf="@id/post_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/like_button"
@ -110,7 +127,7 @@
android:layout_height="wrap_content"
app:icon="@drawable/baseline_info_24" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -218,6 +218,8 @@
<string name="ascending_by_creation_date">older-newer</string>
<string name="descending_by_creation_date">newer-older</string>
<string name="invalid_arg">Invalid value passed</string>
<string name="posting">Posting</string>
<string name="uploading_photos">Uploading photos</string>
<string-array name="report_reasons">
<item>Spam</item>
<item>Explicit content</item>