WIP posts en compose

This commit is contained in:
erik-everardo 2025-01-29 00:41:04 -06:00
parent 5775145a69
commit 37837653cc
28 changed files with 237 additions and 593 deletions

View File

@ -101,8 +101,11 @@ dependencies {
// Customtabs // Customtabs
implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.browser:browser:1.8.0'
implementation 'io.coil-kt:coil:2.5.0' // Coil
implementation 'io.coil-kt:coil-svg:2.5.0' implementation 'io.coil-kt.coil3:coil:3.0.4'
implementation 'io.coil-kt.coil3:coil-svg:3.0.4'
implementation "io.coil-kt.coil3:coil-compose:3.0.1"
implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.1")
implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:core:$markwon_version"
@ -156,8 +159,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5'
implementation 'androidx.compose.runtime:runtime-livedata' implementation 'androidx.compose.runtime:runtime-livedata'
implementation "io.coil-kt.coil3:coil-compose:3.0.1"
implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.1")
implementation("com.google.accompanist:accompanist-permissions:0.36.0") implementation("com.google.accompanist:accompanist-permissions:0.36.0")
} }

View File

@ -1,35 +1,19 @@
package com.isolaatti.common package com.isolaatti.common
import android.content.Context import android.content.Context
import coil.ImageLoader import coil3.ImageLoader
import coil.decode.SvgDecoder
import coil.memory.MemoryCache
import com.isolaatti.MyApplication import com.isolaatti.MyApplication
object CoilImageLoader { object CoilImageLoader {
val imageLoader by lazy { val imageLoader by lazy {
ImageLoader ImageLoader
.Builder(MyApplication.myApp) .Builder(MyApplication.myApp)
.memoryCache { .build()
MemoryCache.Builder(MyApplication.myApp.applicationContext)
.maxSizePercent(0.25)
.build()
}
.components {
add(SvgDecoder.Factory())
}.build()
} }
fun getImageLoader(context: Context): ImageLoader { fun getImageLoader(context: Context): ImageLoader {
return ImageLoader return ImageLoader
.Builder(context) .Builder(context)
.memoryCache { .build()
MemoryCache.Builder(context)
.maxSizePercent(0.25)
.build()
}
.components {
add(SvgDecoder.Factory())
}.build()
} }
} }

View File

@ -8,7 +8,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load import coil3.load
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.CoilImageLoader.imageLoader import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.databinding.ItemUserListBinding import com.isolaatti.databinding.ItemUserListBinding

View File

@ -5,9 +5,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.presentation.PostListingViewModelBase
import com.isolaatti.posting.posts.presentation.UpdateEvent
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

View File

@ -20,7 +20,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import coil.load import coil3.load
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.about.AboutActivity import com.isolaatti.about.AboutActivity

View File

@ -4,8 +4,9 @@ import android.os.Bundle
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 androidx.core.os.BundleCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import coil.load import coil3.load
import com.isolaatti.common.CoilImageLoader.imageLoader import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.databinding.FragmentTouchImageViewWrapperBinding import com.isolaatti.databinding.FragmentTouchImageViewWrapperBinding
import com.isolaatti.images.common.domain.entity.RemoteImage import com.isolaatti.images.common.domain.entity.RemoteImage
@ -24,7 +25,7 @@ class PictureViewerImageWrapperFragment : Fragment() {
): View { ): View {
binding = FragmentTouchImageViewWrapperBinding.inflate(inflater) binding = FragmentTouchImageViewWrapperBinding.inflate(inflater)
image = arguments?.getSerializable(ARGUMENT_IMAGE) as RemoteImage image = BundleCompat.getParcelable(requireArguments(), ARGUMENT_IMAGE, RemoteImage::class.java)
return binding.root return binding.root
} }

View File

@ -4,6 +4,7 @@ import android.os.Bundle
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 androidx.core.content.IntentCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.isolaatti.databinding.FragmentMainPictureViewerBinding import com.isolaatti.databinding.FragmentMainPictureViewerBinding
@ -36,7 +37,7 @@ class PictureViewerMainFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
images = requireActivity().intent.extras?.getSerializable(PictureViewerActivity.EXTRA_IMAGES) as Array<RemoteImage> images = IntentCompat.getParcelableArrayExtra(requireActivity().intent, PictureViewerActivity.EXTRA_IMAGES, RemoteImage::class.java) as Array<RemoteImage>
val position = requireActivity().intent.extras?.getInt(PictureViewerActivity.EXTRA_IMAGE_POSITiON) ?: 0 val position = requireActivity().intent.extras?.getInt(PictureViewerActivity.EXTRA_IMAGE_POSITiON) ?: 0
val adapter = PictureViewerViewPagerAdapter(this, images) val adapter = PictureViewerViewPagerAdapter(this, images)
binding.viewpager.adapter = adapter binding.viewpager.adapter = adapter

View File

@ -31,7 +31,6 @@ class Module {
.create(BuildConfig.backend)) .create(BuildConfig.backend))
} }
}) })
.usePlugin(CoilImagesPlugin.create(context, CoilImageLoader.imageLoader))
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()
} }

View File

@ -6,7 +6,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load import coil3.load
import coil3.request.fallback
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.NotificationItemBinding import com.isolaatti.databinding.NotificationItemBinding
import com.isolaatti.notifications.domain.FollowNotification import com.isolaatti.notifications.domain.FollowNotification

View File

@ -4,7 +4,7 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil3.load
import com.isolaatti.common.CoilImageLoader.imageLoader import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.databinding.CommentLayoutBinding import com.isolaatti.databinding.CommentLayoutBinding
import com.isolaatti.posting.comments.domain.model.Comment import com.isolaatti.posting.comments.domain.model.Comment

View File

@ -217,7 +217,6 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
.create("https://isolaatti.com/")) .create("https://isolaatti.com/"))
} }
}) })
.usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader))
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()

View File

@ -7,7 +7,7 @@ import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import coil.load import coil3.load
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.CoilImageLoader.imageLoader import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.databinding.FragmentEditCommentBinding import com.isolaatti.databinding.FragmentEditCommentBinding
@ -51,7 +51,7 @@ class EditCommentDialogFragment : DialogFragment() {
.create("https://isolaatti.com/")) .create("https://isolaatti.com/"))
} }
}) })
.usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader)) //.usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader))
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()

View File

@ -4,25 +4,22 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -31,11 +28,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.images.common.domain.entity.Image
import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.posting.posts.domain.entity.Post import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.utils.UrlGen.userProfileImage import com.isolaatti.utils.UrlGen.userProfileImage
@ -44,13 +44,17 @@ fun Post(
post: Post, post: Post,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onLike: () -> Unit = {}, onLike: () -> Unit = {},
onDislike: () -> Unit = {},
onComments: () -> Unit = {}, onComments: () -> Unit = {},
onShare: () -> Unit = {}, onShare: () -> Unit = {},
onInfo: () -> Unit = {}, onInfo: () -> Unit = {},
onOptions: () -> Unit = {}, onOptions: () -> Unit = {},
onUsernameClick: () -> Unit = {} onUsernameClick: () -> Unit = {},
onImageClick: (images: List<RemoteImage>, index: Int) -> Unit = {_, _ -> },
onHashtagClick: (hashtag: String) -> Unit = {},
modifier: Modifier = Modifier
) { ) {
Card { Card(modifier = modifier.padding(8.dp)) {
Row(modifier = Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth().padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
AsyncImage( AsyncImage(
model = userProfileImage(post.userId), model = userProfileImage(post.userId),
@ -78,37 +82,80 @@ fun Post(
// TODO audio player here // TODO audio player here
} }
Text(post.textContent, modifier = Modifier.padding(8.dp)) if(post.textContent.isNotBlank()) {
Text(post.textContent, modifier = Modifier.padding(8.dp))
}
// TODO pager
if(post.images.isNotEmpty()) { if(post.images.isNotEmpty()) {
Box(Modifier.fillMaxWidth().aspectRatio(1f)) Box {
val pagerState = rememberPagerState(pageCount = {post.images.size})
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
val image = post.images[it]
AsyncImage(
image.reducedImageUrl, null,
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
.clip(RoundedCornerShape(20.dp))
.clickable {
onImageClick(post.images, it)
}
)
}
if(post.images.size > 1) {
Text("${pagerState.currentPage + 1} of ${pagerState.pageCount}",
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(20.dp)
.background(Color.Black.copy(alpha = 0.5f)),
color = Color.White
)
}
}
} }
Row(modifier = Modifier.height(50.dp).padding(4.dp)) { Row(modifier = Modifier.height(50.dp).padding(4.dp)) {
FilledTonalIconButton(onClick = onLike, modifier = Modifier.padding(4.dp).weight(1f)) { FilledTonalIconButton(
onClick = { if(post.liked) { onDislike() } else { onLike() } },
modifier = Modifier.padding(4.dp).weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon(painterResource(R.drawable.hands_clapping_solid), null, modifier = Modifier.size(30.dp)) Icon(
Column(horizontalAlignment = Alignment.CenterHorizontally) { painterResource(R.drawable.hands_clapping_solid),
Text("Clap") null,
Text(post.numberOfLikes.toString()) modifier = Modifier.padding(horizontal = 8.dp).size(30.dp),
} tint = if(post.liked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
)
Text(
text = post.numberOfLikes.toString(),
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
textAlign = TextAlign.Center
)
} }
} }
FilledTonalIconButton(onClick = onComments, modifier = Modifier.padding(4.dp).weight(1f)) { FilledTonalIconButton(onClick = onComments, modifier = Modifier.padding(4.dp).weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon(painterResource(R.drawable.comments_solid), null, modifier = Modifier.size(30.dp)) Icon(
Column(horizontalAlignment = Alignment.CenterHorizontally) { painterResource(R.drawable.comments_solid),
Text("Comments") null,
Text(post.numberOfComments.toString()) modifier = Modifier.padding(horizontal = 8.dp).size(30.dp)
} )
Text(
text = post.numberOfComments.toString(),
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
textAlign = TextAlign.Center
)
} }
} }
FilledTonalIconButton(onClick = onShare, modifier = Modifier.padding(4.dp).weight(1f)) { FilledTonalIconButton(onClick = onShare, modifier = Modifier.padding(4.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Icon(
Icon(painterResource(R.drawable.baseline_share_24), null, modifier = Modifier.size(30.dp)) painterResource(R.drawable.baseline_share_24),
Text("Share") null,
} )
} }
FilledTonalIconButton(onClick = onInfo, modifier = Modifier.padding(4.dp)) { FilledTonalIconButton(onClick = onInfo, modifier = Modifier.padding(4.dp)) {
Icon(painterResource(R.drawable.baseline_info_24), null) Icon(painterResource(R.drawable.baseline_info_24), null)

View File

@ -2,28 +2,39 @@ package com.isolaatti.posting.posts.domain.entity
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.common.Ownable import com.isolaatti.common.Ownable
import com.isolaatti.common.hashtagRegex import com.isolaatti.common.hashtagRegex
import com.isolaatti.images.common.domain.entity.RemoteImage import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
data class Post( class Post(
val id: Long, val id: Long,
var textContent: String, textContent: String,
override val userId: Int, override val userId: Int,
val privacy: Int, val privacy: Int,
val date: String, val date: String,
var audioId: String? = null, var audioId: String? = null,
val squadId: String? = null, val squadId: String? = null,
var numberOfLikes: Int, numberOfLikes: Int,
var numberOfComments: Int, numberOfComments: Int,
val userName: String, val userName: String,
val squadName: String? = null, val squadName: String? = null,
var liked: Boolean, liked: Boolean,
val audio: Audio? = null, val audio: Audio? = null,
val images: List<RemoteImage> images: List<RemoteImage>
) : Ownable, Parcelable { ) : Ownable, Parcelable {
var liked by mutableStateOf(liked)
var images by mutableStateOf(images)
var textContent by mutableStateOf(textContent)
var numberOfLikes by mutableIntStateOf(numberOfLikes)
var numberOfComments by mutableStateOf(numberOfComments)
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readLong(), parcel.readLong(),
parcel.readString()!!, parcel.readString()!!,

View File

@ -4,7 +4,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load import coil3.load
import com.isolaatti.images.common.domain.entity.RemoteImage import com.isolaatti.images.common.domain.entity.RemoteImage
class PostImagesViewPagerAdapter(private val images: List<RemoteImage>) : class PostImagesViewPagerAdapter(private val images: List<RemoteImage>) :

View File

@ -16,17 +16,13 @@ class PostListingViewModel @Inject constructor(private val postsRepository: Post
override fun getFeed(refresh: Boolean, hashtag: String?) { override fun getFeed(refresh: Boolean, hashtag: String?) {
viewModelScope.launch { viewModelScope.launch {
if (refresh) { if (refresh) {
posts.value = null posts.value = emptyList()
} }
postsRepository.getFeed(getLastId(), hashtag).onEach { listResource -> postsRepository.getFeed(getLastId(), hashtag).onEach { listResource ->
when (listResource) { when (listResource) {
is Resource.Success -> { is Resource.Success -> {
val eventType = if((postsList?.size ?: 0) > 0) UpdateEvent.UpdateType.PAGE_ADDED else UpdateEvent.UpdateType.REFRESH
loadingPosts.postValue(false) loadingPosts.postValue(false)
posts.postValue(Pair(postsList?.apply { posts.value += listResource.data ?: emptyList()
addAll(listResource.data ?: listOf())
} ?: listResource.data,
UpdateEvent(eventType, null)))
noMoreContent.postValue(listResource.data?.size == 0) noMoreContent.postValue(listResource.data?.size == 0)
} }

View File

@ -11,6 +11,7 @@ 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
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -30,9 +31,8 @@ abstract class PostListingViewModelBase : ViewModel() {
@Inject @Inject
lateinit var deletePostUseCase: DeletePost lateinit var deletePostUseCase: DeletePost
val posts: MutableLiveData<Pair<MutableList<Post>?, UpdateEvent>?> = MutableLiveData() val posts: MutableStateFlow<List<Post>> = MutableStateFlow(emptyList())
val postsList get() = posts.value?.first
val loadingPosts = MutableLiveData(false) val loadingPosts = MutableLiveData(false)
@ -41,7 +41,7 @@ abstract class PostListingViewModelBase : ViewModel() {
val errorLoading: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData() val errorLoading: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
var isLoadingFromScrolling = false var isLoadingFromScrolling = false
fun getLastId(): Long = try { posts.value?.first?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 } fun getLastId(): Long = try { posts.value?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 }
abstract fun getFeed(refresh: Boolean, hashtag: String?) abstract fun getFeed(refresh: Boolean, hashtag: String?)
@ -58,18 +58,9 @@ abstract class PostListingViewModelBase : ViewModel() {
Log.i(TAG, "Loading likePost($postId)") Log.i(TAG, "Loading likePost($postId)")
} }
is Resource.Success -> { is Resource.Success -> {
val likedPost = posts.value?.first?.find { post -> post.id == like.data?.postId } val likedPost = posts.value?.find { post -> post.id == like.data?.postId }
val index = posts.value?.first?.indexOf(likedPost) likedPost?.liked = true
if(index != null){ likedPost?.numberOfLikes = like.data!!.likesCount
val temp = posts.value?.first?.toMutableList()
Log.d("***", temp.toString())
temp?.set(index, likedPost!!.apply {
liked = true
numberOfLikes = like.data?.likesCount ?: 0
})
}
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, index)))
} }
} }
@ -90,16 +81,9 @@ abstract class PostListingViewModelBase : ViewModel() {
Log.i(TAG, "Loading unLikePost($postId)") Log.i(TAG, "Loading unLikePost($postId)")
} }
is Resource.Success -> { is Resource.Success -> {
val likedPost = posts.value?.first?.find { post -> post.id == like.data?.postId } val likedPost = posts.value?.find { post -> post.id == like.data?.postId }
val index = posts.value?.first?.indexOf(likedPost) likedPost?.liked = false
if(index != null){ likedPost?.numberOfLikes = like.data!!.likesCount
val temp = posts.value?.first?.toMutableList()
temp?.set(index, likedPost!!.apply {
liked = false
numberOfLikes = like.data?.likesCount ?: 0
})
}
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_LIKED, index)))
} }
} }
@ -113,12 +97,9 @@ abstract class PostListingViewModelBase : ViewModel() {
deletePostUseCase(postId).onEach { res -> deletePostUseCase(postId).onEach { res ->
when(res) { when(res) {
is Resource.Success -> { is Resource.Success -> {
val postDeleted = posts.value?.first?.find { post -> post.id == postId } val postDeleted = posts.value?.find { post -> post.id == postId }
?: return@onEach ?: return@onEach
val index = posts.value?.first?.indexOf(postDeleted) posts.value = posts.value.toMutableList().apply { remove(postDeleted) }
posts.value?.first?.removeAt(index!!)
posts.postValue(posts.value?.copy(second = UpdateEvent(UpdateEvent.UpdateType.POST_REMOVED, index)))
} }
is Resource.Loading -> {} is Resource.Loading -> {}
is Resource.Error -> {} is Resource.Error -> {}

View File

@ -1,402 +0,0 @@
package com.isolaatti.posting.posts.presentation
import android.annotation.SuppressLint
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
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 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
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.isolaatti.R
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.common.OnUserInteractedWithPostCallback
import com.isolaatti.databinding.PostLayoutBinding
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.utils.UrlGen.userProfileImage
class PostsRecyclerViewAdapter (
private val callback: OnUserInteractedWithPostCallback
) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){
private var postList: List<Post>? = null
inner class FeedViewHolder(val itemBinding: PostLayoutBinding) : ViewHolder(itemBinding.root) {
fun bindView(post: Post, payloads: List<Any>) {
if(payloads.isNotEmpty()) {
for(payload in payloads) {
when {
payload is LikeCountUpdatePayload -> {
itemBinding.likeButton.isEnabled = true
if(post.liked) {
itemBinding.likeButton.setIconTintResource(R.color.purple_lighter)
itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter))
} else {
itemBinding.likeButton.setIconTintResource(R.color.on_surface)
itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.on_surface))
}
itemBinding.likeButton.text = post.numberOfLikes.toString()
}
payload is CommentsCountUpdatePayload -> {
itemBinding.commentButton.text = post.numberOfComments.toString()
}
payload is AudioEventPayload && payload == AudioEventPayload.IsPLaying -> {
val audio = post.audio
if(audio != null){
itemBinding.audio.playButton.icon =
AppCompatResources.getDrawable(
itemView.context,
if(audio.isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24
)
}
}
payload is AudioEventPayload && payload == AudioEventPayload.ProgressChanged -> {
val audio = post.audio
if(audio != null){
itemBinding.audio.audioProgress.progress = audio.progress
}
}
payload is AudioEventPayload && payload == AudioEventPayload.IsLoading -> {
val audio = post.audio
if(audio != null){
itemBinding.audio.audioProgress.isIndeterminate = audio.isLoading
}
}
payload is AudioEventPayload && payload == AudioEventPayload.DurationChanged -> {
val audio = post.audio
if(audio != null){
itemBinding.audio.audioProgress.max = audio.duration
}
}
payload is AudioEventPayload && payload == AudioEventPayload.Ended -> {
val audio = post.audio
if(audio != null){
itemBinding.audio.audioProgress.progress = 0
itemBinding.audio.playButton.icon = AppCompatResources.getDrawable(itemView.context, R.drawable.baseline_play_circle_24)
}
}
}
}
} else {
val username: TextView = itemView.findViewById(R.id.text_view_username)
username.text = post.userName
username.setOnClickListener {
callback.onProfileClick(post.userId)
}
val profileImageView: ImageView = itemView.findViewById(R.id.avatar_picture)
profileImageView.load(userProfileImage(post.userId), imageLoader)
val dateTextView: TextView = itemView.findViewById(R.id.text_view_date)
dateTextView.text = post.date
val content: TextView = itemView.findViewById(R.id.post_content)
content.movementMethod = LinkMovementMethod.getInstance()
val spannableString = SpannableString(post.textContent).apply {
post.hashtagsSpans.forEach {
set(it.first, it.last + 1, object: ClickableSpan() {
override fun onClick(widget: View) {
callback.hashtagClicked(post.textContent.substring(it.first + 1, it.last + 1))
}
})
}
}
content.text = spannableString
itemBinding.likeButton.isEnabled = true
if(post.liked) {
itemBinding.likeButton.setIconTintResource(R.color.purple_lighter)
itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.purple_lighter))
} else {
itemBinding.likeButton.setIconTintResource(R.color.on_surface)
itemBinding.likeButton.setTextColor(itemView.context.getColor(R.color.on_surface))
}
itemBinding.likeButton.text = post.numberOfLikes.toString()
itemBinding.commentButton.text = post.numberOfComments.toString()
val moreButton: MaterialButton = itemView.findViewById(R.id.more_button)
moreButton.setOnClickListener {
callback.onOptions(post)
}
itemBinding.likeButton.setOnClickListener {
itemBinding.likeButton.isEnabled = false
if(post.liked){
callback.onUnLiked(post.id)
} else {
callback.onLiked(post.id)
}
}
itemBinding.commentButton.setOnClickListener {
callback.onComment(post.id)
}
itemView.findViewById<MaterialCardView>(R.id.card).setOnClickListener {
callback.onOpenPost(post.id)
}
if(post.audio != null){
itemBinding.audio.apply {
root.visibility = View.VISIBLE
textViewDescription.text = post.audio.name
}
itemBinding.audio.playButton.setOnClickListener {
callback.onPlay(post.audio)
}
} else {
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)
}
itemBinding.infoButton.setOnClickListener {
callback.onMoreInfo(post.id)
}
}
}
}
data class LikeCountUpdatePayload(val likeCount: Int)
data class CommentsCountUpdatePayload(val commentsCount: Int)
private var currentAudio: Audio? = null
private var currentAudioPosition: Int = -1
enum class AudioEventPayload {
ProgressChanged, IsLoading, IsPLaying, DurationChanged, Ended
}
fun setIsPlaying(isPlaying: Boolean, audio: Audio) {
if(audio == currentAudio) {
currentAudio?.isPlaying = isPlaying
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.IsPLaying)
}
return
}
currentAudio?.isPlaying = false
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.IsPLaying)
} else {
if(postList != null) {
for((index, post) in postList!!.withIndex()){
post.audio?.isPlaying = false
post.audio?.progress = 0
post.audio?.isLoading = false
if(post.audio != null) {
notifyItemChanged(index)
}
}
}
}
currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1
Log.d(LOG_TAG, "setIsPlaying currentAudioPosition: $currentAudioPosition")
if(currentAudioPosition > -1) {
currentAudio = postList?.get(currentAudioPosition)?.audio?.also { it.isPlaying = isPlaying }
notifyItemChanged(currentAudioPosition, AudioEventPayload.IsPLaying)
}
}
fun setIsLoading(isLoading: Boolean, audio: Audio) {
if(audio == currentAudio) {
currentAudio?.isLoading = isLoading
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.IsLoading)
}
return
}
currentAudio?.isPlaying = false
currentAudio?.isLoading = false
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.IsLoading)
}
currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1
Log.d(LOG_TAG, "setIsLoading currentAudioPosition: $currentAudioPosition")
if(currentAudioPosition > -1) {
postList?.get(currentAudioPosition)?.audio?.isLoading = isLoading
notifyItemChanged(currentAudioPosition, AudioEventPayload.IsLoading)
}
}
fun setProgress(progress: Int, audio: Audio){
if(audio == currentAudio) {
audio.progress = progress
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
return
}
currentAudio?.isPlaying = false
currentAudio?.progress = 0
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1
if(currentAudioPosition > -1) {
postList?.get(currentAudioPosition)?.audio?.progress = progress
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
}
fun setDuration(duration: Int, audio: Audio) {
if(audio == currentAudio) {
audio.duration = duration
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
return
}
currentAudio?.isPlaying = false
currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1
if(currentAudioPosition > -1) {
postList?.get(currentAudioPosition)?.audio?.duration = duration
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
}
fun setEnded(audio: Audio) {
if(audio == currentAudio) {
audio.isPlaying = false
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
return
}
currentAudio?.isPlaying = false
if(currentAudioPosition > -1) {
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
currentAudioPosition = postList?.indexOf(postList?.find { it.audio == audio }) ?: -1
if(currentAudioPosition > -1) {
postList?.get(currentAudioPosition)?.audio?.isPlaying = false
notifyItemChanged(currentAudioPosition, AudioEventPayload.ProgressChanged)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder {
return FeedViewHolder(PostLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
var previousSize = 0
override fun getItemCount(): Int = postList?.size ?: 0
override fun setHasStableIds(hasStableIds: Boolean) {
super.setHasStableIds(true)
}
@SuppressLint("NotifyDataSetChanged")
fun updateList(updatedFeed: List<Post>, updateEvent: UpdateEvent? = null) {
if(updateEvent == null) {
postList = updatedFeed
notifyDataSetChanged()
return
}
val postUpdated = updateEvent.affectedPosition?.let {
if(updateEvent.updateType == UpdateEvent.UpdateType.POST_REMOVED) {
null
} else {
postList?.get(it)
}
}
val position = updateEvent.affectedPosition
previousSize = itemCount
postList = updatedFeed
when(updateEvent.updateType) {
UpdateEvent.UpdateType.POST_LIKED -> {
if(postUpdated != null && position != null)
notifyItemChanged(position, LikeCountUpdatePayload(postUpdated.numberOfLikes))
}
UpdateEvent.UpdateType.POST_COMMENTED -> {
if(postUpdated != null && position != null)
notifyItemChanged(position, CommentsCountUpdatePayload(postUpdated.numberOfComments))
}
UpdateEvent.UpdateType.POST_REMOVED -> {
if(position != null)
notifyItemRemoved(position)
}
UpdateEvent.UpdateType.POST_ADDED -> {
notifyItemInserted(0)
}
UpdateEvent.UpdateType.PAGE_ADDED -> {
notifyItemInserted(previousSize)
}
UpdateEvent.UpdateType.REFRESH -> {
notifyDataSetChanged()
}
}
}
override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {}
private var requestedNewContent = false
/**
* Call this method when new content has been added on onLoadMore() callback
*/
fun newContentRequestFinished() {
requestedNewContent = false
}
override fun onBindViewHolder(holder: FeedViewHolder, position: Int, payloads: List<Any>) {
holder.bindView(postList?.get(position) ?: return, payloads)
val totalItems = postList?.size
if(totalItems != null && totalItems > 0 && !requestedNewContent) {
if(position == totalItems - 1) {
requestedNewContent = true
if(payloads.isEmpty()) {
callback.onLoadMore()
}
}
}
}
companion object {
const val LOG_TAG = "PostsRecyclerViewAdapter"
}
}

View File

@ -1,12 +0,0 @@
package com.isolaatti.posting.posts.presentation
data class UpdateEvent(val updateType: UpdateType, val affectedPosition: Int?) {
enum class UpdateType {
POST_LIKED,
POST_COMMENTED,
POST_REMOVED,
POST_ADDED,
PAGE_ADDED,
REFRESH
}
}

View File

@ -1,36 +1,48 @@
package com.isolaatti.posting.posts.ui package com.isolaatti.posting.posts.ui
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log 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 androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
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
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.common.Dialogs import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.common.IsolaattiTheme
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.hashtags.ui.HashtagsPostsActivity import com.isolaatti.hashtags.ui.HashtagsPostsActivity
import com.isolaatti.home.ui.FeedFragment import com.isolaatti.home.ui.FeedFragment
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
import com.isolaatti.posting.comments.ui.BottomSheetPostComments 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.EditPostContract import com.isolaatti.posting.posts.presentation.EditPostContract
import com.isolaatti.posting.posts.presentation.PostListingViewModel 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.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.ui.ProfileActivity import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.reports.data.ContentType import com.isolaatti.reports.data.ContentType
@ -38,7 +50,7 @@ import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback { class PostListingFragment : Fragment() {
companion object { companion object {
const val ARG_HASHTAG = "hashtag" const val ARG_HASHTAG = "hashtag"
const val LOG_TAG = "PostListingFragment" const val LOG_TAG = "PostListingFragment"
@ -46,11 +58,9 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
var hashtag: String? = null var hashtag: String? = null
private lateinit var viewBinding: FragmentPostListingBinding
private val errorViewModel: ErrorMessageViewModel by activityViewModels() private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val viewModel: PostListingViewModel by viewModels() private val viewModel: PostListingViewModel by viewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private lateinit var adapter: PostsRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -59,43 +69,75 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
viewModel.getFeed(false, hashtag) viewModel.getFeed(false, hashtag)
} }
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
viewBinding = FragmentPostListingBinding.inflate(inflater, container, false) return ComposeView(requireContext()).apply {
return viewBinding.root setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
var isRefreshing by remember { mutableStateOf(false) }
val posts by viewModel.posts.collectAsState(emptyList())
IsolaattiTheme {
PullToRefreshBox(isRefreshing, onRefresh = {viewModel.getFeed(true, null)}) {
LazyColumn(flingBehavior = ScrollableDefaults.flingBehavior()) {
items(count = posts.size, key = {posts[it].id}) {
val post = posts[it]
com.isolaatti.posting.posts.components.Post(
modifier = Modifier.animateItem(),
post = post,
onClick = {
PostViewerActivity.startActivity(requireContext(), post.id)
},
onComments = {
val modalBottomSheet = BottomSheetPostComments.getInstance(post.id)
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostComments.TAG)
},
onOptions = {
optionsViewModel.setOptions(Options.POST_OPTIONS, FeedFragment.CALLER_ID, post)
val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
},
onInfo = {
PostInfoActivity.startActivity(requireContext(), post.id)
},
onLike = {
viewModel.likePost(post.id)
},
onDislike = {
viewModel.unLikePost(post.id)
},
onShare = {
val intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "${BuildConfig.backend}/pub/${post.id}")
type = "text/plain"
}, getString(R.string.share_post))
startActivity(intent)
},
onImageClick = { images, index ->
PictureViewerActivity.startActivityWithImages(requireContext(), images.toTypedArray(), index)
},
onUsernameClick = {
ProfileActivity.startActivity(requireContext(), post.userId)
},
onHashtagClick = { hashtag ->
HashtagsPostsActivity.startActivity(requireContext(), hashtag)
}
)
}
}
}
}
}
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = PostsRecyclerViewAdapter(this)
viewBinding.feedRecyclerView.adapter = adapter
viewBinding.feedRecyclerView.setItemViewCacheSize(7)
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext())
viewBinding.swipeToRefresh.setOnRefreshListener {
viewModel.getFeed(refresh = true, hashtag)
}
viewModel.posts.observe(viewLifecycleOwner){
if (it?.first != null) {
adapter.updateList(it.first!!, it.second)
adapter.newContentRequestFinished()
}
}
viewModel.loadingPosts.observe(viewLifecycleOwner) {
viewBinding.loadingIndicator.visibility = if(it) View.VISIBLE else View.GONE
if(!it) {
viewBinding.swipeToRefresh.isRefreshing = false
}
}
viewModel.errorLoading.observe(viewLifecycleOwner) { viewModel.errorLoading.observe(viewLifecycleOwner) {
errorViewModel.error.postValue(it) errorViewModel.error.postValue(it)
} }
@ -103,19 +145,17 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
} }
/*
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(post: Ownable) { override fun onOptions(post: Ownable) {
optionsViewModel.setOptions(Options.POST_OPTIONS, FeedFragment.CALLER_ID, post)
val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
} }
override fun onComment(postId: Long) { override fun onComment(postId: Long) {
val modalBottomSheet = BottomSheetPostComments.getInstance(postId)
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostComments.TAG)
} }
override fun onOpenPost(postId: Long) { override fun onOpenPost(postId: Long) {
@ -126,7 +166,7 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
override fun onMoreInfo(postId: Long) { override fun onMoreInfo(postId: Long) {
PostInfoActivity.startActivity(requireContext(), postId)
} }
override fun onShare(postId: Long) { override fun onShare(postId: Long) {
@ -150,6 +190,8 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
viewModel.getFeed(false, hashtag) viewModel.getFeed(false, hashtag)
} }
*/
private val editDiscussion = registerForActivityResult(EditPostContract()) { private val editDiscussion = registerForActivityResult(EditPostContract()) {
if(it != null) { if(it != null) {
viewModel.onPostUpdate(it) viewModel.onPostUpdate(it)

View File

@ -60,10 +60,10 @@ class PostViewerViewModel @Inject constructor(private val loadSinglePost: LoadSi
} }
private fun updateLikesCount(likesCount: Int) { private fun updateLikesCount(likesCount: Int) {
val updatedPost = post.value?.copy(numberOfLikes = likesCount) // val updatedPost = post.value?.copy(numberOfLikes = likesCount)
if(updatedPost != null) { // if(updatedPost != null) {
post.postValue(updatedPost!!) // post.postValue(updatedPost!!)
} // }
} }
fun likeDislikePost() { fun likeDislikePost() {

View File

@ -8,8 +8,8 @@ import android.util.Log
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import coil.imageLoader import coil3.imageLoader
import coil.load import coil3.load
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.IsolaattiBaseActivity import com.isolaatti.common.IsolaattiBaseActivity
@ -153,7 +153,6 @@ class PostViewerActivity : IsolaattiBaseActivity() {
.create(BuildConfig.backend)) .create(BuildConfig.backend))
} }
}) })
.usePlugin(CoilImagesPlugin.create(this, imageLoader))
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()

View File

@ -1,12 +1,12 @@
package com.isolaatti.profile.presentation package com.isolaatti.profile.presentation
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.followers.domain.FollowingState import com.isolaatti.followers.domain.FollowingState
import com.isolaatti.images.common.domain.entity.RemoteImage import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.profile.domain.entity.UserProfile import com.isolaatti.profile.domain.entity.UserProfile
import com.isolaatti.profile.domain.use_case.FollowUser import com.isolaatti.profile.domain.use_case.FollowUser
import com.isolaatti.profile.domain.use_case.GetProfile import com.isolaatti.profile.domain.use_case.GetProfile
@ -116,14 +116,14 @@ class ProfileViewModel @Inject constructor(
override fun getFeed(refresh: Boolean, hashtag: String?) { 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 = emptyList()
getLastId()
} }
Log.d(TAG, "getFeed")
getProfilePostsUseCase(profileId, getLastId(), false, null).onEach { feedDtoResource -> getProfilePostsUseCase(profileId, getLastId(), false, null).onEach { feedDtoResource ->
when (feedDtoResource) { when (feedDtoResource) {
is Resource.Success -> { is Resource.Success -> {
loadingPosts.postValue(false) loadingPosts.postValue(false)
posts.postValue(Pair(posts.value?.first?.apply { addAll(feedDtoResource.data ?: listOf()) } ?: feedDtoResource.data, UpdateEvent(if(refresh) UpdateEvent.UpdateType.REFRESH else UpdateEvent.UpdateType.PAGE_ADDED, null))) posts.value += feedDtoResource.data ?: emptyList()
noMoreContent.postValue(feedDtoResource.data?.size == 0) noMoreContent.postValue(feedDtoResource.data?.size == 0)
} }

View File

@ -17,7 +17,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import coil.load import coil3.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
@ -40,8 +40,6 @@ import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract 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.PostListingRecyclerViewAdapterWiring import com.isolaatti.posting.posts.presentation.PostListingRecyclerViewAdapterWiring
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.posting.posts.ui.PostInfoActivity import com.isolaatti.posting.posts.ui.PostInfoActivity
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.domain.entity.UserProfile import com.isolaatti.profile.domain.entity.UserProfile
@ -67,7 +65,6 @@ class ProfileMainFragment : Fragment() {
val errorViewModel: ErrorMessageViewModel by activityViewModels() val errorViewModel: ErrorMessageViewModel by activityViewModels()
private var userId: Int? = null private var userId: Int? = null
lateinit var postsAdapter: PostsRecyclerViewAdapter
private var audioDescriptionAudio: Audio? = null private var audioDescriptionAudio: Audio? = null
@ -129,13 +126,13 @@ class ProfileMainFragment : Fragment() {
setupUiForUserType(profile.isUserItself) setupUiForUserType(profile.isUserItself)
} }
private val postsObserver: Observer<Pair<List<Post>?, UpdateEvent>?> = Observer { // private val postsObserver: Observer<Pair<List<Post>?, UpdateEvent>?> = Observer {
if(it?.first != null) { // if(it?.first != null) {
postsAdapter.updateList(it.first!!, it.second) // postsAdapter.updateList(it.first!!, it.second)
postsAdapter.newContentRequestFinished() // postsAdapter.newContentRequestFinished()
} // }
//
} // }
private val followingStateObserver: Observer<FollowingState> = Observer { private val followingStateObserver: Observer<FollowingState> = Observer {
when(it) { when(it) {
@ -310,8 +307,8 @@ class ProfileMainFragment : Fragment() {
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToMainFollowersFragment(userId!!)) findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToMainFollowersFragment(userId!!))
} }
viewBinding.feedRecyclerView.adapter = postsAdapter // viewBinding.feedRecyclerView.adapter = postsAdapter
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, null) viewModel.getFeed(true, null)
@ -330,7 +327,7 @@ class ProfileMainFragment : Fragment() {
private fun setObservers() { private fun setObservers() {
viewModel.profile.observe(viewLifecycleOwner, profileObserver) viewModel.profile.observe(viewLifecycleOwner, profileObserver)
viewModel.posts.observe(viewLifecycleOwner, postsObserver) //viewModel.posts.observe(viewLifecycleOwner, postsObserver)
viewModel.followingState.observe(viewLifecycleOwner, followingStateObserver) viewModel.followingState.observe(viewLifecycleOwner, followingStateObserver)
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
viewModel.loadingPosts.observe(viewLifecycleOwner) { viewModel.loadingPosts.observe(viewLifecycleOwner) {
@ -366,11 +363,10 @@ class ProfileMainFragment : Fragment() {
.create(BuildConfig.backend)) .create(BuildConfig.backend))
} }
}) })
.usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader))
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()
postsAdapter = PostsRecyclerViewAdapter(postListingRecyclerViewAdapterWiring ) //postsAdapter = PostsRecyclerViewAdapter(postListingRecyclerViewAdapterWiring )
} }

View File

@ -7,7 +7,9 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.fallback
import coil3.toBitmap
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import com.isolaatti.MyApplication import com.isolaatti.MyApplication

View File

@ -5,7 +5,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load import coil3.load
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.SearchResultItemBinding import com.isolaatti.databinding.SearchResultItemBinding
import com.isolaatti.search.data.SearchResultDto import com.isolaatti.search.data.SearchResultDto

View File

@ -5,7 +5,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load import coil3.load
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.UsersCarouselItemBinding import com.isolaatti.databinding.UsersCarouselItemBinding
import com.isolaatti.profile.domain.entity.ProfileListItem import com.isolaatti.profile.domain.entity.ProfileListItem

View File

@ -7,7 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import coil.load import coil3.load
import com.isolaatti.databinding.FragmentSettingsBinding import com.isolaatti.databinding.FragmentSettingsBinding
import com.isolaatti.settings.presentation.SettingsViewModel import com.isolaatti.settings.presentation.SettingsViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint