Skip to content

Commit

Permalink
Add follows list
Browse files Browse the repository at this point in the history
  • Loading branch information
akiomik committed Mar 22, 2023
1 parent ecd4f7c commit d2d6f0a
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ViewerState(
val muted: Boolean? = null
val muted: Boolean? = null,
val following: String? = null,
val followedBy: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.github.akiomik.seiun.ui.registration.RegistrationScreen
import io.github.akiomik.seiun.ui.timeline.TimelineScreen
import io.github.akiomik.seiun.ui.user.UserScreen
import io.github.akiomik.seiun.viewmodels.AppViewModel
import io.github.akiomik.seiun.viewmodels.FollowsViewModel

@Composable
fun AppNavigation(
Expand Down Expand Up @@ -109,9 +110,11 @@ fun AppNavigation(
"user/{did}",
arguments = listOf(navArgument("did") { type = NavType.StringType })
) {
val did = it.arguments?.getString("did")!!
UserScreen(
it.arguments?.getString("did")!!,
onProfileClick = { did -> navController.navigate("user/$did") }
did,
followsViewModel = FollowsViewModel(did),
onProfileClick = { navController.navigate("user/$it") }
)
}
composable("login") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.akiomik.seiun.viewmodels.UserFeedViewModel

@Composable
fun LoadingIndicator() {
fun FeedLoadingIndicator() {
val viewModel: UserFeedViewModel = viewModel()
val context = LocalContext.current

Expand Down
113 changes: 113 additions & 0 deletions app/src/main/java/io/github/akiomik/seiun/ui/user/FollowsListModal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package io.github.akiomik.seiun.ui.user

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import io.github.akiomik.seiun.R
import io.github.akiomik.seiun.model.app.bsky.actor.RefWithInfo
import io.github.akiomik.seiun.ui.dialog.FullScreenDialog
import io.github.akiomik.seiun.viewmodels.FollowsViewModel

@Composable
private fun Avatar(user: RefWithInfo, onClicked: (String) -> Unit) {
AsyncImage(
model = user.avatar,
contentDescription = null,
modifier = Modifier
.width(56.dp)
.height(56.dp)
.clip(CircleShape)
.clickable { onClicked(user.did) }
)
}

@Composable
private fun FollowsListItem(user: RefWithInfo, onProfileClick: (String) -> Unit) {
ListItem(
leadingContent = { Avatar(user = user, onClicked = onProfileClick) },
headlineContent = { Text(text = user.displayName ?: "@${user.handle}") },
supportingContent = {
Text(
text = "@${user.handle}",
color = Color.Gray,
style = MaterialTheme.typography.labelMedium
)
},
trailingContent = {
// TODO
// if (user.viewer?.following == null) {
// Button(onClick = { /*TODO*/ }) {
// Text(stringResource(R.string.follow))
// }
// } else {
// Button(onClick = { /*TODO*/ }) {
// Text(stringResource(R.string.unfollow))
// }
// }
}
)
}

@Composable
private fun FollowsListContent(
follows: List<RefWithInfo>,
viewModel: FollowsViewModel,
onProfileClick: (String) -> Unit
) {
val seenAllFollows by viewModel.seenAllFollows.collectAsState()

LazyColumn {
items(follows) {
FollowsListItem(user = it, onProfileClick = onProfileClick)
Divider(color = Color.Gray)
}

if (follows.isEmpty()) {
item { Text(stringResource(R.string.follows_no_follows_yet)) }
} else if (!seenAllFollows) {
item { FollowsLoadingIndicator(viewModel) }
}
}
}

// TODO: Implement as Screen
@Composable
fun FollowsListModal(
viewModel: FollowsViewModel,
onClose: () -> Unit,
onProfileClick: (String) -> Unit
) {
val state by viewModel.state.collectAsState()
val follows by viewModel.follows.collectAsState()

FullScreenDialog(onClose = onClose, actions = {}) {
when (state) {
is FollowsViewModel.State.Loading -> {
CircularProgressIndicator()
}
is FollowsViewModel.State.Loaded -> {
FollowsListContent(follows, viewModel, onProfileClick)
}
is FollowsViewModel.State.Error -> {
Text((state as FollowsViewModel.State.Error).error.toString())
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.github.akiomik.seiun.ui.user

import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.akiomik.seiun.viewmodels.FollowsViewModel

@Composable
fun FollowsLoadingIndicator(viewModel: FollowsViewModel) {
val context = LocalContext.current

Box(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}

LaunchedEffect(key1 = true) {
viewModel.loadMoreFollows(onError = {
Toast.makeText(context, it.toString(), Toast.LENGTH_LONG).show()
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fun UserFeedContent(onProfileClick: (String) -> Unit) {
} else if (seenAllFeed) {
item { NoMorePostsMessage() }
} else {
item { LoadingIndicator() }
item { FeedLoadingIndicator() }
}
}

Expand Down
49 changes: 41 additions & 8 deletions app/src/main/java/io/github/akiomik/seiun/ui/user/UserScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.akiomik.seiun.ui.user

import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -42,6 +43,7 @@ import io.github.akiomik.seiun.R
import io.github.akiomik.seiun.model.app.bsky.actor.ProfileDetail
import io.github.akiomik.seiun.ui.theme.Indigo800
import io.github.akiomik.seiun.viewmodels.AppViewModel
import io.github.akiomik.seiun.viewmodels.FollowsViewModel
import io.github.akiomik.seiun.viewmodels.UserFeedViewModel
import me.onebone.toolbar.CollapsingToolbarScaffold
import me.onebone.toolbar.ScrollStrategy
Expand Down Expand Up @@ -96,7 +98,21 @@ private fun NameAndHandle(profile: ProfileDetail) {
}

@Composable
private fun StatRow(profile: ProfileDetail) {
private fun StatRow(
profile: ProfileDetail,
followsViewModel: FollowsViewModel,
onProfileClick: (String) -> Unit
) {
var showFollowsList by remember { mutableStateOf(false) }

if (showFollowsList) {
FollowsListModal(
viewModel = followsViewModel,
onProfileClick = onProfileClick,
onClose = { showFollowsList = false }
)
}

Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
Expand All @@ -105,7 +121,8 @@ private fun StatRow(profile: ProfileDetail) {
)
Text(
text = stringResource(R.string.follows),
style = MaterialTheme.typography.labelMedium
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.clickable { showFollowsList = true }
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Expand Down Expand Up @@ -158,7 +175,11 @@ private fun FollowOrUnfollowButton(profile: ProfileDetail) {
}

@Composable
private fun Profile(profile: ProfileDetail) {
private fun Profile(
profile: ProfileDetail,
followsViewModel: FollowsViewModel,
onProfileClick: (String) -> Unit
) {
val viewModel: AppViewModel = viewModel()
val viewer by viewModel.profile.collectAsState()
var showEditProfile by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -189,12 +210,20 @@ private fun Profile(profile: ProfileDetail) {

Text(profile.description.orEmpty())

StatRow(profile = profile)
StatRow(
profile = profile,
followsViewModel = followsViewModel,
onProfileClick = onProfileClick
)
}
}

@Composable
private fun UserModalContent(profile: ProfileDetail, onProfileClick: (String) -> Unit) {
private fun UserModalContent(
profile: ProfileDetail,
followsViewModel: FollowsViewModel,
onProfileClick: (String) -> Unit
) {
val bannerHeight = 128.dp
val avatarSize = 96.dp

Expand All @@ -216,7 +245,7 @@ private fun UserModalContent(profile: ProfileDetail, onProfileClick: (String) ->
.background(color = MaterialTheme.colorScheme.surfaceVariant)
.fillMaxWidth()
) {
Profile(profile)
Profile(profile, followsViewModel, onProfileClick)
}
Divider()
}
Expand All @@ -230,7 +259,7 @@ private fun UserModalContent(profile: ProfileDetail, onProfileClick: (String) ->
}

@Composable
fun UserScreen(did: String, onProfileClick: (String) -> Unit) {
fun UserScreen(did: String, followsViewModel: FollowsViewModel, onProfileClick: (String) -> Unit) {
val userFeedViewModel: UserFeedViewModel = viewModel()
var profileRequested by remember { mutableStateOf(false) }
val profile by userFeedViewModel.profile.collectAsState()
Expand All @@ -249,7 +278,11 @@ fun UserScreen(did: String, onProfileClick: (String) -> Unit) {
CircularProgressIndicator()
}
} else {
UserModalContent(profile = profile!!, onProfileClick = onProfileClick)
UserModalContent(
profile = profile!!,
followsViewModel = followsViewModel,
onProfileClick = onProfileClick
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.github.akiomik.seiun.viewmodels

import android.util.Log
import io.github.akiomik.seiun.SeiunApplication
import io.github.akiomik.seiun.model.app.bsky.actor.RefWithInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

class FollowsViewModel(val did: String) : ApplicationViewModel() {
sealed class State {
object Loading : State()
object Loaded : State()
data class Error(val error: Throwable) : State()
}

private val userRepository = SeiunApplication.instance!!.userRepository
private var _cursor: String? = null

private val _seenAllFollows = MutableStateFlow(false)
private val _state = MutableStateFlow<State>(State.Loading)
private val _follows = MutableStateFlow<List<RefWithInfo>>(emptyList())
val seenAllFollows = _seenAllFollows.asStateFlow()
val state = _state.asStateFlow()
val follows = _follows.asStateFlow()

init {
wrapError(
run = { userRepository.getFollows(did) },
onSuccess = {
Log.d(SeiunApplication.TAG, it.toString())
_follows.value = it.follows
_cursor = it.cursor
_state.value = State.Loaded
},
onError = {
Log.d(SeiunApplication.TAG, it.toString())
_state.value = State.Error(it)
}
)
}

fun loadMoreFollows(onError: (Throwable) -> Unit = {}) {
wrapError(run = {
val data = userRepository.getFollows(did = did, before = _cursor)

if (data.cursor != _cursor) {
if (data.follows.isNotEmpty()) {
_follows.value = _follows.value + data.follows
_cursor = data.cursor
Log.d(SeiunApplication.TAG, "Feed posts are updated")
} else {
Log.d(SeiunApplication.TAG, "No new feed posts")
_seenAllFollows.value = true
}
}
}, onError = onError)
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values-ja-rJP/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@
<string name="dialog_updated">更新しました</string>
<string name="notification_name">投稿とアクティビティ</string>
<string name="notification_title">新しい通知が %1$d 件あります</string>
<string name="follows_no_follows_yet">まだフォローがいません</string>
</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@
<string name="dialog_updated">Updated</string>
<string name="notification_name">Post and activities</string>
<string name="notification_title">You\'ve got %1$d recent notifications</string>
<string name="follows_no_follows_yet">No follows yet</string>
</resources>

0 comments on commit d2d6f0a

Please sign in to comment.