fix: opening member list makes app crash sometimes

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-10-02 02:11:10 +02:00
parent 8212dd0fe5
commit 88a5a3e14a
3 changed files with 131 additions and 30 deletions

View File

@ -7,6 +7,7 @@ import chat.revolt.api.internals.ULID
import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.Channel
import chat.revolt.api.schemas.Message import chat.revolt.api.schemas.Message
import chat.revolt.api.schemas.MessagesInChannel import chat.revolt.api.schemas.MessagesInChannel
import chat.revolt.api.schemas.User
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.parameter import io.ktor.client.request.parameter
import io.ktor.client.request.patch import io.ktor.client.request.patch
@ -136,3 +137,13 @@ suspend fun fetchSingleChannel(channelId: String): Channel {
response response
) )
} }
suspend fun fetchGroupParticipants(channelId: String): List<User> {
val response = RevoltHttp.get("/channels/$channelId/members")
.bodyAsText()
return RevoltJson.decodeFromString(
ListSerializer(User.serializer()),
response
)
}

View File

@ -52,7 +52,8 @@ fun ChannelInfoSheet(
} }
) { ) {
MemberListSheet( MemberListSheet(
serverId = channel?.server ?: "" channelId = channelId,
serverId = channel?.server,
) )
} }
} }
@ -100,6 +101,8 @@ fun ChannelInfoSheet(
modifier = Modifier.padding(bottom = 4.dp) modifier = Modifier.padding(bottom = 4.dp)
) )
when (channel.channelType) {
ChannelType.TextChannel, ChannelType.VoiceChannel, ChannelType.Group -> {
SheetClickable( SheetClickable(
icon = { modifier -> icon = { modifier ->
Icon( Icon(
@ -117,6 +120,10 @@ fun ChannelInfoSheet(
) { ) {
memberListSheetShown = true memberListSheetShown = true
} }
}
else -> {}
}
SheetClickable( SheetClickable(
icon = { modifier -> icon = { modifier ->

View File

@ -46,13 +46,17 @@ import androidx.lifecycle.viewModelScope
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.REVOLT_FILES import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.PermissionBit
import chat.revolt.api.internals.Roles import chat.revolt.api.internals.Roles
import chat.revolt.api.internals.WebCompat import chat.revolt.api.internals.WebCompat
import chat.revolt.api.internals.hasPermission
import chat.revolt.api.internals.solidColor import chat.revolt.api.internals.solidColor
import chat.revolt.api.routes.channel.fetchGroupParticipants
import chat.revolt.api.routes.server.fetchMembers import chat.revolt.api.routes.server.fetchMembers
import chat.revolt.api.schemas.Member import chat.revolt.api.schemas.Member
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import chat.revolt.components.generic.PageHeader import chat.revolt.components.generic.PageHeader
import chat.revolt.components.generic.Presence
import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.presenceFromStatus import chat.revolt.components.generic.presenceFromStatus
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -67,6 +71,7 @@ val DO_NOT_FETCH_OFFLINE_MEMBERS_SERVERS = listOf(
sealed class MemberListItem { sealed class MemberListItem {
data class MemberItem(val member: Member) : MemberListItem() data class MemberItem(val member: Member) : MemberListItem()
data class UserItem(val user: User) : MemberListItem()
data class CategoryItem(val category: String, val count: Int) : MemberListItem() data class CategoryItem(val category: String, val count: Int) : MemberListItem()
} }
@ -77,14 +82,16 @@ class MemberListSheetViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val fullItemList = mutableStateListOf<MemberListItem>() val fullItemList = mutableStateListOf<MemberListItem>()
fun fetchMemberList( fun fetchServerMemberList(
serverId: String serverId: String,
channelId: String
) { ) {
viewModelScope.launch { viewModelScope.launch {
val memberList = fetchMembers( val memberList = fetchMembers(
serverId = serverId, serverId = serverId,
includeOffline = serverId !in DO_NOT_FETCH_OFFLINE_MEMBERS_SERVERS includeOffline = serverId !in DO_NOT_FETCH_OFFLINE_MEMBERS_SERVERS
).members ).members
val channel = RevoltAPI.channelCache[channelId] ?: return@launch
val categories = mutableMapOf<String, List<Member>>() val categories = mutableMapOf<String, List<Member>>()
@ -115,6 +122,12 @@ class MemberListSheetViewModel @Inject constructor(
defaultCategoryName defaultCategoryName
} }
if (!Roles.permissionFor(channel, user, member)
.hasPermission(PermissionBit.ViewChannel)
) {
return@forEach
}
categories[category] = (categories[category] ?: listOf()) + member categories[category] = (categories[category] ?: listOf()) + member
} }
@ -156,12 +169,60 @@ class MemberListSheetViewModel @Inject constructor(
} }
} }
} }
fun fetchGroupMemberList(channelId: String) {
viewModelScope.launch {
val userList = fetchGroupParticipants(channelId)
val onlinePredicate = { user: User ->
presenceFromStatus(
user.status?.presence,
user.online ?: false
) != Presence.Offline
}
val offlinePredicate = { user: User ->
presenceFromStatus(
user.status?.presence,
user.online ?: false
) == Presence.Offline
}
fullItemList.clear()
if (userList.count(onlinePredicate) > 0) {
fullItemList.add(
MemberListItem.CategoryItem(
context.getString(R.string.status_online),
userList.count(onlinePredicate)
)
)
userList.filter(onlinePredicate).forEach { user ->
fullItemList.add(MemberListItem.UserItem(user))
}
}
if (userList.count(offlinePredicate) > 0) {
fullItemList.add(
MemberListItem.CategoryItem(
context.getString(R.string.status_offline),
userList.count(offlinePredicate)
)
)
userList.filter(offlinePredicate).forEach { user ->
fullItemList.add(MemberListItem.UserItem(user))
}
}
}
}
} }
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun MemberListSheet( fun MemberListSheet(
serverId: String, channelId: String,
serverId: String? = null,
viewModel: MemberListSheetViewModel = hiltViewModel() viewModel: MemberListSheetViewModel = hiltViewModel()
) { ) {
var showUserContextSheet by remember { mutableStateOf(false) } var showUserContextSheet by remember { mutableStateOf(false) }
@ -170,7 +231,11 @@ fun MemberListSheet(
// We use LaunchedEffect to make sure that this is called every time any of the users status changes // We use LaunchedEffect to make sure that this is called every time any of the users status changes
LaunchedEffect(RevoltAPI.userCache) { LaunchedEffect(RevoltAPI.userCache) {
snapshotFlow { RevoltAPI.userCache }.distinctUntilChanged().collect { snapshotFlow { RevoltAPI.userCache }.distinctUntilChanged().collect {
viewModel.fetchMemberList(serverId) if (serverId != null) {
viewModel.fetchServerMemberList(serverId, channelId)
} else {
viewModel.fetchGroupMemberList(channelId)
}
} }
} }
@ -211,9 +276,21 @@ fun MemberListSheet(
} }
is MemberListItem.MemberItem -> item(key = item.member.id!!.user) { is MemberListItem.MemberItem -> item(key = item.member.id!!.user) {
MemberListMember( MemberListMemberUser(
member = item.member,
user = RevoltAPI.userCache[item.member.id.user]!!, user = RevoltAPI.userCache[item.member.id.user]!!,
member = item.member,
serverId = serverId,
onSelectUser = {
userContextSheetTarget = it
showUserContextSheet = true
}
)
}
is MemberListItem.UserItem -> item(key = item.user.id!!) {
MemberListMemberUser(
user = item.user,
member = null,
serverId = serverId, serverId = serverId,
onSelectUser = { onSelectUser = {
userContextSheetTarget = it userContextSheetTarget = it
@ -228,32 +305,38 @@ fun MemberListSheet(
} }
@Composable @Composable
fun MemberListMember( fun MemberListMemberUser(
member: Member,
user: User, user: User,
serverId: String, member: Member?,
serverId: String?,
onSelectUser: (String) -> Unit onSelectUser: (String) -> Unit
) { ) {
val highestColourRole = Roles.resolveHighestRole(serverId, member.id!!.user, true) val highestColourRole = serverId?.let {
Roles.resolveHighestRole(
it,
user.id!!,
true
)
}
val colour = highestColourRole?.colour?.let { WebCompat.parseColour(it) } val colour = highestColourRole?.colour?.let { WebCompat.parseColour(it) }
?: Brush.solidColor(LocalContentColor.current) ?: Brush.solidColor(LocalContentColor.current)
Row( Row(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
onSelectUser(member.id.user) onSelectUser(user.id!!)
} }
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp), .padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
UserAvatar( UserAvatar(
username = member.nickname username = member?.nickname
?: user.displayName ?: user.displayName
?: user.username ?: user.username
?: user.id!!, ?: user.id!!,
avatar = user.avatar, avatar = user.avatar,
rawUrl = member.avatar?.let { "$REVOLT_FILES/avatars/${it.id}?max_side=256" }, rawUrl = member?.avatar?.let { "$REVOLT_FILES/avatars/${it.id}?max_side=256" },
userId = user.id!!, userId = user.id!!,
presence = presenceFromStatus( presence = presenceFromStatus(
user.status?.presence, user.status?.presence,
@ -264,7 +347,7 @@ fun MemberListMember(
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = member.nickname text = member?.nickname
?: user.displayName ?: user.displayName
?: user.username ?: user.username
?: user.id, ?: user.id,