WIP perfil en compose
This commit is contained in:
parent
37837653cc
commit
b019c79bcb
@ -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))
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 = {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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 -> {
|
||||||
loadingPosts.postValue(false)
|
if(refresh) {
|
||||||
|
isRefreshing.value = false
|
||||||
|
} else {
|
||||||
|
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 -> {
|
||||||
loadingPosts.postValue(true)
|
if(refresh) {
|
||||||
|
isRefreshing.value = true
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
|||||||
@ -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,121 +107,324 @@ 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
|
|
||||||
if(profile.descriptionText.isNullOrBlank()) {
|
|
||||||
viewBinding.descriptionCard.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.goToFollowersBtn.text = getString(
|
userId?.let { profileId ->
|
||||||
R.string.go_to_followers_btn_text,
|
viewModel.profileId = profileId
|
||||||
profile.numberOfFollowers.toString(),
|
viewModel.getProfile()
|
||||||
profile.numberOfFollowing.toString()
|
viewModel.getFeed(true, null)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
viewBinding.profileImageView.setOnClickListener {
|
|
||||||
optionsViewModel.setOptions(Options.PROFILE_PHOTO_OPTIONS, CALLER_ID, profile)
|
|
||||||
val fragment = BottomSheetPostOptionsFragment()
|
|
||||||
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
audioDescriptionAudio = profile.descriptionAudio
|
|
||||||
viewBinding.playButtonContainer.visibility = if(profile.descriptionAudio != null) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
setupUiForUserType(profile.isUserItself)
|
|
||||||
}
|
|
||||||
|
|
||||||
// private val postsObserver: Observer<Pair<List<Post>?, UpdateEvent>?> = Observer {
|
|
||||||
// if(it?.first != null) {
|
|
||||||
// postsAdapter.updateList(it.first!!, it.second)
|
|
||||||
// postsAdapter.newContentRequestFinished()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
private val followingStateObserver: Observer<FollowingState> = Observer {
|
|
||||||
when(it) {
|
|
||||||
FollowingState.FollowingThisUser -> {
|
|
||||||
viewBinding.textViewFollowingState.setText(R.string.following_user)
|
|
||||||
viewBinding.followButton.setText(R.string.unfollow)
|
|
||||||
viewBinding.followButton.isChecked = true
|
|
||||||
}
|
|
||||||
FollowingState.MutuallyFollowing -> {
|
|
||||||
viewBinding.textViewFollowingState.setText(R.string.mutually_following)
|
|
||||||
viewBinding.followButton.setText(R.string.unfollow)
|
|
||||||
viewBinding.followButton.isChecked = true
|
|
||||||
}
|
|
||||||
FollowingState.ThisUserIsFollowingMe -> {
|
|
||||||
viewBinding.textViewFollowingState.setText(R.string.following_you)
|
|
||||||
viewBinding.followButton.setText(R.string.follow)
|
|
||||||
viewBinding.followButton.isChecked = false
|
|
||||||
}
|
|
||||||
FollowingState.NotMutuallyFollowing -> {
|
|
||||||
viewBinding.textViewFollowingState.text = ""
|
|
||||||
viewBinding.followButton.setText(R.string.follow)
|
|
||||||
viewBinding.followButton.isChecked = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked ->
|
private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
|
||||||
if(optionClicked?.callerId == CALLER_ID) {
|
private lateinit var mediaController: MediaController
|
||||||
Log.d("ProfileMainFragment", optionClicked.toString())
|
|
||||||
|
|
||||||
when(optionClicked.optionsId) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Options.PROFILE_PHOTO_OPTIONS -> {
|
super.onCreate(savedInstanceState)
|
||||||
val profile = optionClicked.payload as? UserProfile
|
|
||||||
when(optionClicked.optionId) {
|
val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), MediaService::class.java))
|
||||||
Options.Option.OPTION_PROFILE_PHOTO_CHANGE_PHOTO -> {
|
mediaControllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
|
||||||
|
|
||||||
|
userId = (requireActivity()).intent.extras?.getInt(ProfileActivity.EXTRA_USER_ID)
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show FAB
|
||||||
|
if (available.y > 1) {
|
||||||
|
showAddPostButton = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(profile) {
|
||||||
|
showAddPostButton = profile?.isUserItself == true
|
||||||
|
}
|
||||||
|
|
||||||
|
val followingText = when(followingState) {
|
||||||
|
FollowingState.FollowingThisUser -> {
|
||||||
|
stringResource(R.string.following_user)
|
||||||
|
}
|
||||||
|
FollowingState.MutuallyFollowing -> {
|
||||||
|
stringResource(R.string.mutually_following)
|
||||||
|
}
|
||||||
|
FollowingState.ThisUserIsFollowingMe -> {
|
||||||
|
stringResource(R.string.following_you)
|
||||||
|
}
|
||||||
|
FollowingState.NotMutuallyFollowing -> {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Options.Option.OPTION_PROFILE_PHOTO_REMOVE_PHOTO -> {
|
) { insets ->
|
||||||
showRemoveProfileImageDialog()
|
|
||||||
}
|
PullToRefreshBox(
|
||||||
Options.Option.OPTION_PROFILE_PHOTO_VIEW_PHOTO -> {
|
isRefreshing,
|
||||||
val profilePictureUrl = profile?.profilePictureUrl
|
onRefresh = {
|
||||||
if(profilePictureUrl != null) {
|
viewModel.getProfile()
|
||||||
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(
|
viewModel.getFeed(refresh = true, hashtag = null)
|
||||||
RemoteImage(profile.profileImageId ?: "")
|
},
|
||||||
))
|
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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
optionsViewModel.handle()
|
|
||||||
}
|
|
||||||
Options.POST_OPTIONS -> {
|
|
||||||
// post id should come as payload
|
|
||||||
val post = optionClicked.payload as? Post ?: return@Observer
|
|
||||||
when(optionClicked.optionId) {
|
|
||||||
Options.Option.OPTION_DELETE -> {
|
|
||||||
Dialogs.buildDeletePostDialog(requireContext()) { delete ->
|
|
||||||
optionsViewModel.handle()
|
|
||||||
if(delete) {
|
|
||||||
viewModel.deletePost(post.id)
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
}
|
|
||||||
Options.Option.OPTION_EDIT -> {
|
|
||||||
optionsViewModel.handle()
|
|
||||||
editDiscussion.launch(post.id)
|
|
||||||
}
|
|
||||||
Options.Option.OPTION_REPORT -> {
|
|
||||||
optionsViewModel.handle()
|
|
||||||
NewReportBottomSheetDialogFragment.newInstance(ContentType.Post, post.id.toString()).show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Options.PROFILE_DESCRIPTION_OPTIONS -> {
|
|
||||||
optionsViewModel.handle()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
Log.d("ProfileMainFragment", optionClicked.toString())
|
||||||
|
|
||||||
|
when(optionClicked.optionsId) {
|
||||||
|
Options.PROFILE_PHOTO_OPTIONS -> {
|
||||||
|
val profile = optionClicked.payload as? UserProfile
|
||||||
|
when(optionClicked.optionId) {
|
||||||
|
Options.Option.OPTION_PROFILE_PHOTO_CHANGE_PHOTO -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
Options.Option.OPTION_PROFILE_PHOTO_REMOVE_PHOTO -> {
|
||||||
|
showRemoveProfileImageDialog()
|
||||||
|
}
|
||||||
|
Options.Option.OPTION_PROFILE_PHOTO_VIEW_PHOTO -> {
|
||||||
|
profile?.profileImage?.let {
|
||||||
|
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optionsViewModel.handle()
|
||||||
|
}
|
||||||
|
Options.POST_OPTIONS -> {
|
||||||
|
// post id should come as payload
|
||||||
|
val post = optionClicked.payload as? Post ?: return@observe
|
||||||
|
when(optionClicked.optionId) {
|
||||||
|
Options.Option.OPTION_DELETE -> {
|
||||||
|
Dialogs.buildDeletePostDialog(requireContext()) { delete ->
|
||||||
|
optionsViewModel.handle()
|
||||||
|
if(delete) {
|
||||||
|
viewModel.deletePost(post.id)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
|
||||||
|
}
|
||||||
|
Options.Option.OPTION_EDIT -> {
|
||||||
|
optionsViewModel.handle()
|
||||||
|
//editDiscussion.launch(post.id)
|
||||||
|
}
|
||||||
|
Options.Option.OPTION_REPORT -> {
|
||||||
|
NewReportBottomSheetDialogFragment
|
||||||
|
.newInstance(ContentType.Post, post.id.toString())
|
||||||
|
.show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
|
||||||
|
optionsViewModel.handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Options.PROFILE_DESCRIPTION_OPTIONS -> {
|
||||||
|
optionsViewModel.handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user