WIP perfil en compose

This commit is contained in:
erik-everardo 2025-02-03 22:20:16 -06:00
parent 37837653cc
commit b019c79bcb
11 changed files with 581 additions and 462 deletions

View File

@ -4,7 +4,6 @@ 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.fillMaxSize
@ -34,13 +33,12 @@ 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.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
@Composable @Composable
fun Post( fun PostComponent(
post: Post, post: Post,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onLike: () -> Unit = {}, onLike: () -> Unit = {},
@ -54,7 +52,7 @@ fun Post(
onHashtagClick: (hashtag: String) -> Unit = {}, onHashtagClick: (hashtag: String) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Card(modifier = modifier.padding(8.dp)) { Card(modifier = modifier.padding(8.dp).clickable { onClick() }) {
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),
@ -169,5 +167,5 @@ fun Post(
@Composable @Composable
@Preview(device = Devices.PIXEL_5) @Preview(device = Devices.PIXEL_5)
fun PostPreview() { fun PostPreview() {
Post(Post(id = 0L, textContent = "Test", userId = 1, privacy = 2, date = "Date", images = emptyList(), liked = false, userName = "Username", numberOfLikes = 1, numberOfComments = 2)) PostComponent(Post(id = 0L, textContent = "Test", userId = 1, privacy = 2, date = "Date", images = emptyList(), liked = false, userName = "Username", numberOfLikes = 1, numberOfComments = 2))
} }

View File

@ -33,7 +33,6 @@ abstract class PostListingViewModelBase : ViewModel() {
val posts: MutableStateFlow<List<Post>> = MutableStateFlow(emptyList()) val posts: MutableStateFlow<List<Post>> = MutableStateFlow(emptyList())
val loadingPosts = MutableLiveData(false) val loadingPosts = MutableLiveData(false)
val noMoreContent = MutableLiveData(false) val noMoreContent = MutableLiveData(false)
@ -114,6 +113,6 @@ abstract class PostListingViewModelBase : ViewModel() {
} }
fun onPostAddedAtTheBeginning(post: Post) { fun onPostAddedAtTheBeginning(post: Post) {
posts.value = listOf(post) + posts.value
} }
} }

View File

@ -40,6 +40,7 @@ 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.images.picture_viewer.ui.PictureViewerActivity
import com.isolaatti.posting.comments.ui.BottomSheetPostComments import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.posting.posts.components.PostComponent
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
@ -85,7 +86,7 @@ class PostListingFragment : Fragment() {
LazyColumn(flingBehavior = ScrollableDefaults.flingBehavior()) { LazyColumn(flingBehavior = ScrollableDefaults.flingBehavior()) {
items(count = posts.size, key = {posts[it].id}) { items(count = posts.size, key = {posts[it].id}) {
val post = posts[it] val post = posts[it]
com.isolaatti.posting.posts.components.Post( PostComponent(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
post = post, post = post,
onClick = { onClick = {

View File

@ -2,6 +2,7 @@ package com.isolaatti.profile.domain.entity
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.images.common.domain.entity.RemoteImage
import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.utils.UrlGen import com.isolaatti.utils.UrlGen
import java.io.Serializable import java.io.Serializable
@ -26,6 +27,8 @@ data class UserProfile(
val profileAvatarPictureUrl: String get() = UrlGen.userProfileImage(userId) val profileAvatarPictureUrl: String get() = UrlGen.userProfileImage(userId)
val profilePictureUrl: String get() = UrlGen.userProfileImageFullQuality(userId) val profilePictureUrl: String get() = UrlGen.userProfileImageFullQuality(userId)
val profileImage: RemoteImage? get() = profileImageId?.let { imageId -> RemoteImage(imageId) }
companion object { companion object {
fun fromDto(userProfileDto: UserProfileDto): UserProfile { fun fromDto(userProfileDto: UserProfileDto): UserProfile {
return UserProfile( return UserProfile(

View File

@ -16,6 +16,7 @@ import com.isolaatti.profile.domain.use_case.SetProfileImage
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
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
@ -32,6 +33,7 @@ class ProfileViewModel @Inject constructor(
) : PostListingViewModelBase() { ) : PostListingViewModelBase() {
private val _profile = MutableLiveData<UserProfile>() private val _profile = MutableLiveData<UserProfile>()
val profile: LiveData<UserProfile> get() = _profile val profile: LiveData<UserProfile> get() = _profile
val loadingProfile: MutableStateFlow<Boolean> = MutableStateFlow(true)
var profileId: Int = 0 var profileId: Int = 0
@ -41,6 +43,8 @@ class ProfileViewModel @Inject constructor(
val followingLoading: MutableLiveData<Boolean> = MutableLiveData() val followingLoading: MutableLiveData<Boolean> = MutableLiveData()
val isRefreshing: MutableStateFlow<Boolean> = MutableStateFlow(false)
// runs the lists of "Runnable" one by one and clears list. After this is executed, // runs the lists of "Runnable" one by one and clears list. After this is executed,
// caller should report as handled // caller should report as handled
@ -57,13 +61,17 @@ class ProfileViewModel @Inject constructor(
getProfileUseCase(profileId).onEach { getProfileUseCase(profileId).onEach {
when(it) { when(it) {
is Resource.Error -> { is Resource.Error -> {
loadingProfile.value = false
errorLoading.postValue(it.errorType) errorLoading.postValue(it.errorType)
toRetry.add { toRetry.add {
getProfile() getProfile()
} }
} }
is Resource.Loading -> {} is Resource.Loading -> {
loadingProfile.value = true
}
is Resource.Success -> { is Resource.Success -> {
loadingProfile.value = false
_profile.postValue(it.data!!) _profile.postValue(it.data!!)
followingState.postValue( followingState.postValue(
it.data.let { user-> getFollowingState(user.followingThisUser, user.thisUserIsFollowingMe) } it.data.let { user-> getFollowingState(user.followingThisUser, user.thisUserIsFollowingMe) }
@ -118,20 +126,34 @@ class ProfileViewModel @Inject constructor(
if(refresh) { if(refresh) {
posts.value = emptyList() posts.value = emptyList()
} }
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 -> {
if(refresh) {
isRefreshing.value = false
} else {
loadingPosts.postValue(false) loadingPosts.postValue(false)
}
posts.value += feedDtoResource.data ?: emptyList() posts.value += feedDtoResource.data ?: emptyList()
noMoreContent.postValue(feedDtoResource.data?.size == 0) noMoreContent.postValue(feedDtoResource.data?.size == 0)
} }
is Resource.Loading -> { is Resource.Loading -> {
if(refresh) {
isRefreshing.value = true
} else {
loadingPosts.postValue(true) loadingPosts.postValue(true)
} }
}
is Resource.Error -> { is Resource.Error -> {
if(refresh) {
isRefreshing.value = true
} else {
loadingPosts.postValue(true)
}
errorLoading.postValue(feedDtoResource.errorType) errorLoading.postValue(feedDtoResource.errorType)
toRetry.add { toRetry.add {
getFeed(refresh, hashtag) getFeed(refresh, hashtag)

View File

@ -1,6 +1,6 @@
package com.isolaatti.profile.ui package com.isolaatti.profile.ui
import android.content.Context import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -8,72 +8,89 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
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.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
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.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.media3.session.MediaController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.media3.session.SessionToken
import coil3.load import androidx.navigation.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.common.util.concurrent.ListenableFuture
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.player.MediaService
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.common.Dialogs import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.common.Ownable import com.isolaatti.common.IsolaattiTheme
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.FragmentDiscussionsBinding
import com.isolaatti.followers.domain.FollowingState import com.isolaatti.followers.domain.FollowingState
import com.isolaatti.hashtags.ui.HashtagsPostsActivity import com.isolaatti.hashtags.ui.HashtagsPostsActivity
import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity 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.components.PostComponent
import com.isolaatti.posting.posts.domain.entity.Post import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.presentation.EditPostContract
import com.isolaatti.posting.posts.presentation.PostListingRecyclerViewAdapterWiring
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
import com.isolaatti.profile.presentation.EditProfileContract import com.isolaatti.profile.presentation.EditProfileContract
import com.isolaatti.profile.presentation.ProfileViewModel import com.isolaatti.profile.presentation.ProfileViewModel
import com.isolaatti.profile.ui.components.ProfileHeader
import com.isolaatti.reports.data.ContentType import com.isolaatti.reports.data.ContentType
import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
import com.isolaatti.utils.UrlGen
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin import kotlinx.coroutines.guava.await
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.coil.CoilImagesPlugin
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class ProfileMainFragment : Fragment() { class ProfileMainFragment : Fragment() {
lateinit var viewBinding: FragmentDiscussionsBinding
private val viewModel: ProfileViewModel by viewModels() private val viewModel: ProfileViewModel by viewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
val errorViewModel: ErrorMessageViewModel by activityViewModels() val errorViewModel: ErrorMessageViewModel by activityViewModels()
private var userId: Int? = null private var userId: Int? = null
private var audioDescriptionAudio: Audio? = null
// collapsing bar
private var title = ""
private var scrollRange = -1
private var isShow = false
private val createDiscussion = registerForActivityResult(CreatePostContract()) { private val createDiscussion = registerForActivityResult(CreatePostContract()) {
if(it == null) if(it == null)
return@registerForActivityResult return@registerForActivityResult
@ -83,13 +100,6 @@ class ProfileMainFragment : Fragment() {
viewModel.onPostAddedAtTheBeginning(it) viewModel.onPostAddedAtTheBeginning(it)
} }
private val editDiscussion = registerForActivityResult(EditPostContract()) {
if(it != null) {
viewModel.onPostUpdate(it)
}
}
private val editProfile = registerForActivityResult(EditProfileContract()) { updatedProfile -> private val editProfile = registerForActivityResult(EditProfileContract()) { updatedProfile ->
if(updatedProfile != null) { if(updatedProfile != null) {
viewModel.setProfile(updatedProfile) viewModel.setProfile(updatedProfile)
@ -97,69 +107,271 @@ class ProfileMainFragment : Fragment() {
} }
private val profileObserver = Observer<UserProfile> { profile ->
viewBinding.profileImageView.load(UrlGen.userProfileImage(profile.userId, invalidateCache = true), imageLoader)
title = profile.name private fun getData() {
viewBinding.textViewUsername.text = profile.name
viewBinding.textViewDescription.text = profile.descriptionText userId?.let { profileId ->
if(profile.descriptionText.isNullOrBlank()) { viewModel.profileId = profileId
viewBinding.descriptionCard.visibility = View.GONE viewModel.getProfile()
viewModel.getFeed(true, null)
}
} }
viewBinding.goToFollowersBtn.text = getString( private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
R.string.go_to_followers_btn_text, private lateinit var mediaController: MediaController
profile.numberOfFollowers.toString(),
profile.numberOfFollowing.toString()
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding.profileImageView.setOnClickListener { val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), MediaService::class.java))
optionsViewModel.setOptions(Options.PROFILE_PHOTO_OPTIONS, CALLER_ID, profile) mediaControllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
val fragment = BottomSheetPostOptionsFragment()
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG) userId = (requireActivity()).intent.extras?.getInt(ProfileActivity.EXTRA_USER_ID)
getData()
} }
audioDescriptionAudio = profile.descriptionAudio
viewBinding.playButtonContainer.visibility = if(profile.descriptionAudio != null) View.VISIBLE else View.GONE
setupUiForUserType(profile.isUserItself) @OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val isRefreshing by viewModel.isRefreshing.collectAsState()
val lazyColumState = rememberLazyListState()
val profile by viewModel.profile.observeAsState()
val followingState by viewModel.followingState.observeAsState()
val isAtTop by remember {
derivedStateOf {
lazyColumState.firstVisibleItemIndex == 0 && lazyColumState.firstVisibleItemScrollOffset == 0
}
}
var showAddPostButton by remember { mutableStateOf(false) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
// Hide FAB
if (available.y < -1) {
showAddPostButton = false
} }
// private val postsObserver: Observer<Pair<List<Post>?, UpdateEvent>?> = Observer { // Show FAB
// if(it?.first != null) { if (available.y > 1) {
// postsAdapter.updateList(it.first!!, it.second) showAddPostButton = true
// postsAdapter.newContentRequestFinished() }
// }
//
// }
private val followingStateObserver: Observer<FollowingState> = Observer { return Offset.Zero
when(it) { }
}
}
LaunchedEffect(profile) {
showAddPostButton = profile?.isUserItself == true
}
val followingText = when(followingState) {
FollowingState.FollowingThisUser -> { FollowingState.FollowingThisUser -> {
viewBinding.textViewFollowingState.setText(R.string.following_user) stringResource(R.string.following_user)
viewBinding.followButton.setText(R.string.unfollow)
viewBinding.followButton.isChecked = true
} }
FollowingState.MutuallyFollowing -> { FollowingState.MutuallyFollowing -> {
viewBinding.textViewFollowingState.setText(R.string.mutually_following) stringResource(R.string.mutually_following)
viewBinding.followButton.setText(R.string.unfollow)
viewBinding.followButton.isChecked = true
} }
FollowingState.ThisUserIsFollowingMe -> { FollowingState.ThisUserIsFollowingMe -> {
viewBinding.textViewFollowingState.setText(R.string.following_you) stringResource(R.string.following_you)
viewBinding.followButton.setText(R.string.follow)
viewBinding.followButton.isChecked = false
} }
FollowingState.NotMutuallyFollowing -> { FollowingState.NotMutuallyFollowing -> {
viewBinding.textViewFollowingState.text = "" ""
viewBinding.followButton.setText(R.string.follow) }
viewBinding.followButton.isChecked = false
null -> ""
}
val posts by viewModel.posts.collectAsState()
val loadingProfile by viewModel.loadingProfile.collectAsState()
val playingAudio by remember { mutableStateOf(true) }
IsolaattiTheme {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
activity?.finish()
}
) {
Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.back_button))
}
},
title = {
Text(if(isAtTop) stringResource(R.string.profile) else profile?.name ?: "")
},
actions = {
IconButton(
onClick = {
profile?.let { findNavController().navigate(ProfileMainFragmentDirections.actionMainFragmentToQrCodeFragment(it)) }
}
) {
Icon(painterResource(R.drawable.baseline_qr_code_24), stringResource(R.string.qr_code))
}
}
)
},
floatingActionButton = {
AnimatedVisibility(
visible = showAddPostButton,
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 })
) {
FloatingActionButton(
onClick = {
createDiscussion.launch(Unit)
}
) {
Icon(Icons.Default.Add, stringResource(R.string.add_post_button_desc))
}
}
}
) { insets ->
PullToRefreshBox(
isRefreshing,
onRefresh = {
viewModel.getProfile()
viewModel.getFeed(refresh = true, hashtag = null)
},
modifier = Modifier
.padding(insets)
.consumeWindowInsets(insets)
) {
LazyColumn(
state = lazyColumState,
modifier = Modifier
.fillMaxWidth()
.then(if(profile?.isUserItself == true) { Modifier.nestedScroll(nestedScrollConnection) } else Modifier)
) {
item {
profile?.let {
ProfileHeader(
modifier = Modifier.padding(16.dp),
name = it.name,
description = it.descriptionText,
image = it.profileImage,
audio = it.descriptionAudio,
followingCount = it.numberOfFollowing,
followerCount = it.numberOfFollowers,
loading = loadingProfile,
onImageClick = {
optionsViewModel.setOptions(Options.PROFILE_PHOTO_OPTIONS, CALLER_ID, it)
val fragment = BottomSheetPostOptionsFragment()
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
},
onFollowersClick = {
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToMainFollowersFragment(it.userId))
},
followingText = followingText,
followingThisUser = it.followingThisUser,
isOwnUser = it.isUserItself,
onFollowChange = { follow ->
viewModel.followUser()
},
profileAudioProgress = 0.4f,
showProfileAudioProgress = true,
onEditProfileClick = {
editProfile.launch(it)
}
)
}
}
if(posts.isEmpty()) {
item {
Text(getString(R.string.you_will_see_what_user_posts_here, profile?.name ?: ""), modifier = Modifier.padding(24.dp))
}
}
items(count = posts.size, key = {posts[it].id}) {
val post = posts[it]
PostComponent(
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, 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 = {
if(profile?.userId == post.userId) return@PostComponent
ProfileActivity.startActivity(requireContext(), post.userId)
},
onHashtagClick = { hashtag ->
HashtagsPostsActivity.startActivity(requireContext(), hashtag)
}
)
}
}
}
}
}
} }
} }
} }
private val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked -> override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
mediaController = mediaControllerFuture.await()
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
viewModel.retry()
errorViewModel.handleRetry()
}
}
}
optionsViewModel.optionClicked.observe(viewLifecycleOwner) { optionClicked ->
if(optionClicked?.callerId == CALLER_ID) { if(optionClicked?.callerId == CALLER_ID) {
Log.d("ProfileMainFragment", optionClicked.toString()) Log.d("ProfileMainFragment", optionClicked.toString())
@ -174,11 +386,8 @@ class ProfileMainFragment : Fragment() {
showRemoveProfileImageDialog() showRemoveProfileImageDialog()
} }
Options.Option.OPTION_PROFILE_PHOTO_VIEW_PHOTO -> { Options.Option.OPTION_PROFILE_PHOTO_VIEW_PHOTO -> {
val profilePictureUrl = profile?.profilePictureUrl profile?.profileImage?.let {
if(profilePictureUrl != null) { PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(it))
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(
RemoteImage(profile.profileImageId ?: "")
))
} }
} }
} }
@ -186,7 +395,7 @@ class ProfileMainFragment : Fragment() {
} }
Options.POST_OPTIONS -> { Options.POST_OPTIONS -> {
// post id should come as payload // post id should come as payload
val post = optionClicked.payload as? Post ?: return@Observer val post = optionClicked.payload as? Post ?: return@observe
when(optionClicked.optionId) { when(optionClicked.optionId) {
Options.Option.OPTION_DELETE -> { Options.Option.OPTION_DELETE -> {
Dialogs.buildDeletePostDialog(requireContext()) { delete -> Dialogs.buildDeletePostDialog(requireContext()) { delete ->
@ -199,11 +408,13 @@ class ProfileMainFragment : Fragment() {
} }
Options.Option.OPTION_EDIT -> { Options.Option.OPTION_EDIT -> {
optionsViewModel.handle() optionsViewModel.handle()
editDiscussion.launch(post.id) //editDiscussion.launch(post.id)
} }
Options.Option.OPTION_REPORT -> { Options.Option.OPTION_REPORT -> {
NewReportBottomSheetDialogFragment
.newInstance(ContentType.Post, post.id.toString())
.show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
optionsViewModel.handle() optionsViewModel.handle()
NewReportBottomSheetDialogFragment.newInstance(ContentType.Post, post.id.toString()).show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
} }
} }
} }
@ -213,6 +424,8 @@ class ProfileMainFragment : Fragment() {
} }
}
} }
} }
@ -226,254 +439,6 @@ class ProfileMainFragment : Fragment() {
} }
.show() .show()
} }
private lateinit var postListingRecyclerViewAdapterWiring: PostListingRecyclerViewAdapterWiring
// counter variable to prevent calling appBarLayout.totalScrollRange many times
private var i = 0
private fun setupCollapsingBar() {
viewBinding.topAppBarLayout.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
// prevent calling appBarLayout.totalScrollRange many times
if (i == 30) {
scrollRange = appBarLayout.totalScrollRange
i = 0
} else {
i++
}
if (scrollRange + verticalOffset == 0) {
viewBinding.collapsingToolbarLayout.title = viewModel.profile.value?.name
isShow = true
} else if (isShow) {
viewBinding.collapsingToolbarLayout.title = " "
}
}
}
private fun showBlockUserDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.block_profile)
.setMessage(getString(R.string.block_profile_dialog_message, viewModel.profile.value?.name ?: ""))
.setPositiveButton(R.string.yes_continue) {_, _ ->
// TODO implement
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun bind() {
if(userId == null) {
return
}
viewBinding.topAppBar.setNavigationOnClickListener {
requireActivity().finish()
}
viewBinding.topAppBar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.edit_profile -> {
viewModel.profile.value?.let { profile -> editProfile.launch(profile) }
true
}
R.id.user_link_menu_item -> {
viewModel.profile.value?.let { profile ->
findNavController().navigate(ProfileMainFragmentDirections.actionMainFragmentToQrCodeFragment(profile))
}
true
}
R.id.report_profile_menu_item -> {
NewReportBottomSheetDialogFragment.newInstance(ContentType.Profile, viewModel.profileId.toString()).show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
true
}
R.id.block_profile_menu_item -> {
showBlockUserDialog()
true
}
else -> false
}
}
viewBinding.createPostButton.setOnClickListener {
createDiscussion.launch(Unit)
}
viewBinding.goToFollowersBtn.setOnClickListener {
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToMainFollowersFragment(userId!!))
}
// viewBinding.feedRecyclerView.adapter = postsAdapter
// viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewBinding.swipeToRefresh.setOnRefreshListener {
viewModel.getFeed(true, null)
}
viewBinding.playButton.setOnClickListener {
audioDescriptionAudio?.let { audio ->
}
}
viewBinding.followButton.setOnClickListener {
it.isEnabled = false
viewModel.followUser()
}
}
private fun setObservers() {
viewModel.profile.observe(viewLifecycleOwner, profileObserver)
//viewModel.posts.observe(viewLifecycleOwner, postsObserver)
viewModel.followingState.observe(viewLifecycleOwner, followingStateObserver)
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
viewModel.loadingPosts.observe(viewLifecycleOwner) {
viewBinding.loadingIndicator.visibility = if(it) View.VISIBLE else View.GONE
if(!it) {
viewBinding.swipeToRefresh.isRefreshing = false
}
}
viewModel.errorLoading.observe(viewLifecycleOwner) {
errorViewModel.error.postValue(it)
}
viewModel.followingLoading.observe(viewLifecycleOwner) {
viewBinding.followButton.isEnabled = !it
}
}
private fun getData() {
userId?.let { profileId ->
viewModel.profileId = profileId
viewModel.getProfile()
viewModel.getFeed(true, null)
}
}
private fun setupPostsAdapter() {
val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder
.imageDestinationProcessor(
ImageDestinationProcessorRelativeToAbsolute
.create(BuildConfig.backend))
}
})
.usePlugin(LinkifyPlugin.create())
.build()
//postsAdapter = PostsRecyclerViewAdapter(postListingRecyclerViewAdapterWiring )
}
private fun setupUiForUserType(isOwnProfile: Boolean) {
if(isOwnProfile) {
viewBinding.followButton.visibility = View.GONE
viewBinding.topAppBar.menu?.run {
removeItem(R.id.block_profile_menu_item)
removeItem(R.id.report_profile_menu_item)
}
viewBinding.descriptionCard.setOnClickListener {
optionsViewModel.setOptions(Options.PROFILE_DESCRIPTION_OPTIONS, CALLER_ID, null)
val fragment = BottomSheetPostOptionsFragment()
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
}
} else {
viewBinding.createPostButton.visibility = View.GONE
viewBinding.topAppBar.menu.removeItem(R.id.edit_profile)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userId = (requireActivity()).intent.extras?.getInt(ProfileActivity.EXTRA_USER_ID)
getData()
}
override fun onAttach(context: Context) {
super.onAttach(context)
postListingRecyclerViewAdapterWiring = object: PostListingRecyclerViewAdapterWiring(viewModel) {
override fun onComment(postId: Long) {
val modalBottomSheet = BottomSheetPostComments.getInstance(postId)
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostComments.TAG)
}
override fun onOpenPost(postId: Long) {
PostViewerActivity.startActivity(requireContext(), postId)
}
override fun onOptions(post: Ownable) {
optionsViewModel.setOptions(Options.POST_OPTIONS, CALLER_ID, post)
val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
}
override fun onProfileClick(userId: Int) {
//ProfileActivity.startActivity(requireContext(), userId)
}
override fun onPlay(audio: Audio) {
}
override fun onLoadMore() {
viewModel.getFeed(false, null)
}
override fun onMoreInfo(postId: Long) {
PostInfoActivity.startActivity(requireContext(), postId)
}
override fun onShare(postId: Long) {
val intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "${BuildConfig.backend}/pub/${postId}")
type = "text/plain"
}, getString(R.string.share_post))
startActivity(intent)
}
override fun hashtagClicked(hashtag: String) {
HashtagsPostsActivity.startActivity(requireContext(), hashtag)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = FragmentDiscussionsBinding.inflate(inflater)
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPostsAdapter()
bind()
setObservers()
setupCollapsingBar()
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
viewModel.retry()
errorViewModel.handleRetry()
}
}
}
}
companion object { companion object {
const val CALLER_ID = 30 const val CALLER_ID = 30
const val LOG_TAG = "ProfileMainFragment" const val LOG_TAG = "ProfileMainFragment"

View File

@ -5,10 +5,15 @@ 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 android.view.WindowInsets
import androidmads.library.qrgenearator.BuildConfig import androidmads.library.qrgenearator.BuildConfig
import androidmads.library.qrgenearator.QRGContents import androidmads.library.qrgenearator.QRGContents
import androidmads.library.qrgenearator.QRGEncoder import androidmads.library.qrgenearator.QRGEncoder
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@ -49,6 +54,13 @@ class QrCodeFragment : Fragment() {
binding.toolbar.setNavigationOnClickListener { binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack() findNavController().popBackStack()
} }
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
val topPadding = insets.getInsets(WindowInsetsCompat.Type.systemBars())
binding.toolbar.updatePadding(top = topPadding.top)
insets
}
} }
private fun generateUrl(): String { private fun generateUrl(): String {

View File

@ -0,0 +1,173 @@
package com.isolaatti.profile.ui.components
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateValue
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.isolaatti.R
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.images.common.domain.entity.RemoteImage
@Composable
private fun ProfileHeaderLoading() {
val infiniteTransition = rememberInfiniteTransition()
val tonalElevation by infiniteTransition.animateValue(
initialValue = 6.dp,
targetValue = 30.dp,
typeConverter = Dp.VectorConverter,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
), label = "tonal elevation for loading skeleton"
)
Surface(modifier = Modifier.fillMaxSize(), tonalElevation = tonalElevation) {}
}
@Composable
fun ProfileHeader(
name: String,
description: String?,
image: RemoteImage?,
audio: Audio?,
followingText: String?,
followerCount: Int,
followingCount: Int,
onImageClick: () -> Unit,
showProfileAudioProgress: Boolean,
profileAudioProgress: Float,
onFollowersClick: () -> Unit,
onEditProfileClick: () -> Unit,
onFollowChange: (Boolean) -> Unit,
loading: Boolean = false,
isOwnUser: Boolean,
followingThisUser: Boolean,
modifier: Modifier = Modifier
) {
Box(modifier) {
if(loading) {
ProfileHeaderLoading()
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
if(image != null) {
AsyncImage(
model = image.reducedImageUrl,
contentDescription = stringResource(R.string.profile_photo),
modifier = Modifier
.size(140.dp)
.padding(16.dp)
.clip(RoundedCornerShape(50))
.clickable {
onImageClick()
}
)
} else {
Image(
painterResource(R.drawable.avatar),
contentDescription = stringResource(R.string.profile_photo),
modifier = Modifier
.size(140.dp)
.padding(16.dp)
.clip(RoundedCornerShape(50))
.clickable {
onImageClick()
}
)
}
}
if(!isOwnUser) {
followingText?.let {
Text(it)
}
}
Text(name, fontSize = 20.sp, modifier = Modifier.padding(top = 16.dp))
description?.let {
Text(it, modifier = Modifier.padding(top = 8.dp))
}
TextButton(onClick = onFollowersClick, modifier = Modifier.padding(top = 4.dp)) {
Text(stringResource(R.string.go_to_followers_btn_text, followerCount, followingCount))
}
when {
isOwnUser -> {
OutlinedButton(onClick = onEditProfileClick) {
Icon(Icons.Default.Edit, null)
Text(stringResource(R.string.edit_profile))
}
}
followingThisUser -> {
OutlinedButton(onClick = { onFollowChange(false) }) {
Text(stringResource(R.string.unfollow))
}
}
else -> {
Button(onClick = { onFollowChange(true) }) {
Text(stringResource(R.string.follow))
}
}
}
}
}
}
}
@Composable
@Preview(device = Devices.PIXEL_3)
fun ProfileHeaderPreview() {
ProfileHeader(
name = "Erik",
description = "Hola",
image = null,
audio = null,
followingText = "Mutually following",
followerCount = 100,
followingCount = 100,
isOwnUser = true,
followingThisUser = true,
onImageClick = {},
profileAudioProgress = 0.6f,
showProfileAudioProgress = true,
onFollowersClick = {},
onFollowChange = {},
onEditProfileClick = {}
)
}

View File

@ -58,62 +58,7 @@
app:layout_constraintTop_toBottomOf="@id/text_view_following_state" app:layout_constraintTop_toBottomOf="@id/text_view_following_state"
tools:text="Erik Cavazos" /> tools:text="Erik Cavazos" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/description_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="16dp"
style="?attr/materialCardViewFilledStyle"
app:layout_constraintTop_toBottomOf="@id/text_view_username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/play_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/audio_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.button.MaterialButton
android:id="@+id/play_button"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_play_circle_24" />
</RelativeLayout>
<TextView
android:id="@+id/text_view_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginHorizontal="20dp"
android:maxLines="4"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/play_button_container"
app:layout_constraintTop_toTopOf="parent"
tools:text="Hi there, I am software developer!" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/go_to_followers_btn" android:id="@+id/go_to_followers_btn"
@ -122,7 +67,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
style="@style/Widget.Material3.Button.TextButton" style="@style/Widget.Material3.Button.TextButton"
app:layout_constraintTop_toBottomOf="@id/description_card"/> app:layout_constraintTop_toBottomOf="@id/text"/>
@ -175,17 +120,14 @@
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.compose.ui.platform.ComposeView
android:id="@+id/swipe_to_refresh" android:id="@+id/compose_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:clipToPadding="false"> android:clipToPadding="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view" </androidx.compose.ui.platform.ComposeView>
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator" android:id="@+id/loading_indicator"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple">#4d3b68</color> <color name="purple">#A77EE4</color>
<color name="purple_lighter">#3F0095</color> <color name="purple_lighter">#3F0095</color>
<color name="surface">#1D1725</color> <color name="surface">#1D1725</color>
<color name="on_surface">#FFFFFF</color> <color name="on_surface">#FFFFFF</color>

View File

@ -226,6 +226,10 @@
<string name="pause_audio">Pause audio</string> <string name="pause_audio">Pause audio</string>
<string name="play_audio">Play audio</string> <string name="play_audio">Play audio</string>
<string name="just_recorded_audio_title">Just recorded audio</string> <string name="just_recorded_audio_title">Just recorded audio</string>
<string name="back_button">Back button</string>
<string name="profile">Profile</string>
<string name="add_post_button_desc">Add post button</string>
<string name="you_will_see_what_user_posts_here">When %s posts something you will see it here</string>
<string-array name="report_reasons"> <string-array name="report_reasons">
<item>Spam</item> <item>Spam</item>
<item>Explicit content</item> <item>Explicit content</item>