feat: group create screen

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-02-21 01:21:51 +01:00
parent b746c1d47e
commit a5739f0f58
9 changed files with 299 additions and 2 deletions

View File

@ -51,6 +51,7 @@ import chat.revolt.screens.SplashScreen
import chat.revolt.screens.about.AboutScreen
import chat.revolt.screens.about.AttributionScreen
import chat.revolt.screens.chat.ChatRouterScreen
import chat.revolt.screens.create.CreateGroupScreen
import chat.revolt.screens.labs.LabsRootScreen
import chat.revolt.screens.login.LoginGreetingScreen
import chat.revolt.screens.login.LoginScreen
@ -365,6 +366,8 @@ fun AppEntrypoint(
)
}
composable("create/group") { CreateGroupScreen(navController) }
composable("discover") { DiscoverScreen(navController) }
composable("settings") { SettingsScreen(navController) }

View File

@ -30,7 +30,7 @@ object FriendRequests {
fun getFriends(excludeOnline: Boolean = false): List<User> {
return RevoltAPI.userCache.values.filter { user ->
user.relationship == "Friend" && (excludeOnline && user.online == false)
user.relationship == "Friend" && if (excludeOnline) user.online == false else true
}
}
}

View File

@ -0,0 +1,40 @@
package chat.revolt.api.routes.channel
import chat.revolt.api.RevoltError
import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson
import chat.revolt.api.schemas.Channel
import chat.revolt.screens.create.MAX_ADDABLE_PEOPLE_IN_GROUP
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
@Serializable
data class CreateGroupDMBody(
val name: String,
val users: List<String>
)
suspend fun createGroupDM(name: String, members: List<String>): Channel {
if (members.size > MAX_ADDABLE_PEOPLE_IN_GROUP) {
throw Exception("Too many members, maximum is $MAX_ADDABLE_PEOPLE_IN_GROUP")
}
val response = RevoltHttp.post("/channels/create") {
contentType(ContentType.Application.Json)
setBody(CreateGroupDMBody(name, members))
}.bodyAsText()
try {
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
throw Error(error.type)
} catch (e: SerializationException) {
// Not an error
}
return RevoltJson.decodeFromString(Channel.serializer(), response)
}

View File

@ -24,6 +24,7 @@ fun MemberListItem(
serverId: String?,
userId: String,
modifier: Modifier = Modifier,
trailingContent: @Composable (() -> Unit)? = null,
) {
val highestColourRole = serverId?.let {
user?.id?.let { userId ->
@ -79,5 +80,6 @@ fun MemberListItem(
)
)
},
trailingContent = trailingContent
)
}

View File

@ -1038,6 +1038,7 @@ fun ChannelNavigator(
toggleDrawer()
}
FriendsScreen(
topNav = topNav,
useDrawer = useDrawer,
onDrawerClicked = toggleDrawer,
)

View File

@ -25,8 +25,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.internals.FriendRequests
import chat.revolt.api.routes.user.unfriendUser
@ -40,7 +42,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun FriendsScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) {
fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: () -> Unit) {
var overflowMenuShown by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
@ -67,6 +69,14 @@ fun FriendsScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) {
}
},
actions = {
IconButton(onClick = {
topNav.navigate("create/group")
}) {
Icon(
painter = painterResource(R.drawable.ic_account_multiple_plus_24dp),
contentDescription = stringResource(R.string.frends_new_group)
)
}
IconButton(onClick = {
overflowMenuShown = true
}) {

View File

@ -0,0 +1,225 @@
package chat.revolt.screens.create
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.activities.RevoltTweenFloat
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.FriendRequests
import chat.revolt.api.routes.channel.createGroupDM
import chat.revolt.callbacks.Action
import chat.revolt.callbacks.ActionChannel
import chat.revolt.components.chat.MemberListItem
import kotlinx.coroutines.launch
const val MAX_PEOPLE_IN_GROUP = 50
const val MAX_ADDABLE_PEOPLE_IN_GROUP = MAX_PEOPLE_IN_GROUP - 1
class CreateGroupScreenViewModel : ViewModel() {
var groupName by mutableStateOf("")
var groupMembers = mutableStateListOf<String>()
var friendSearchQuery by mutableStateOf("")
var friendsFilteredBySearch = mutableStateListOf<String>()
var error by mutableStateOf<String?>(null)
fun updateFriendSearchQuery(query: String) {
friendSearchQuery = query
filterFriends()
}
fun filterFriends() {
friendsFilteredBySearch.clear()
friendsFilteredBySearch.addAll(FriendRequests.getFriends().filter {
if (friendSearchQuery.isBlank()) {
return@filter true
}
if (it.displayName == null || it.username == null) {
return@filter false
}
it.displayName.contains(friendSearchQuery, ignoreCase = true) ||
it.username.contains(friendSearchQuery, ignoreCase = true)
}.map { it.id!! })
}
fun createGroup(popBackStack: () -> Unit) {
if (groupMembers.size > MAX_ADDABLE_PEOPLE_IN_GROUP) {
error = "Too many members, maximum is $MAX_ADDABLE_PEOPLE_IN_GROUP"
return
}
try {
error = null
viewModelScope.launch {
val channel = createGroupDM(groupName, groupMembers)
popBackStack()
channel.id?.let { ActionChannel.send(Action.SwitchChannel(it)) }
}
} catch (e: Exception) {
error = e.message
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateGroupScreen(
navController: NavController,
viewModel: CreateGroupScreenViewModel = viewModel()
) {
LaunchedEffect(Unit) {
viewModel.filterFriends()
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.create_group),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(id = R.string.back)
)
}
}
)
},
floatingActionButton = {
AnimatedVisibility(
visible = viewModel.groupName.isNotBlank() && viewModel.groupMembers.isNotEmpty(),
enter = scaleIn(animationSpec = RevoltTweenFloat),
exit = scaleOut(animationSpec = RevoltTweenFloat)
) {
FloatingActionButton(onClick = { viewModel.createGroup(navController::popBackStack) }) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = stringResource(R.string.create_group_action)
)
}
}
}
) { pv ->
Column(
Modifier
.padding(pv)
.imePadding()
) {
Text(
text = stringResource(R.string.create_group_description, MAX_PEOPLE_IN_GROUP),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
AnimatedVisibility(visible = viewModel.error?.isNotBlank() ?: false) {
Text(
text = viewModel.error ?: "",
color = MaterialTheme.colorScheme.error,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
TextField(
value = viewModel.groupName,
onValueChange = { viewModel.groupName = it },
label = { Text(stringResource(R.string.create_group_name)) },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp)
)
OutlinedTextField(
value = viewModel.friendSearchQuery,
onValueChange = { viewModel.updateFriendSearchQuery(it) },
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null
)
},
label = { Text(stringResource(R.string.create_group_search)) },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
)
LazyColumn(contentPadding = PaddingValues(bottom = 78.0.dp)) {
items(viewModel.friendsFilteredBySearch.size) { index ->
val friend = RevoltAPI.userCache[viewModel.friendsFilteredBySearch[index]]
?: return@items
val isMember = viewModel.groupMembers.contains(friend.id)
MemberListItem(
member = null,
user = friend,
serverId = null,
userId = friend.id!!,
modifier = Modifier.clickable {
if (isMember) {
viewModel.groupMembers.remove(friend.id)
} else {
if (viewModel.groupMembers.size < MAX_ADDABLE_PEOPLE_IN_GROUP) {
viewModel.groupMembers.add(friend.id)
}
}
},
trailingContent = {
Checkbox(
checked = isMember,
onCheckedChange = null,
enabled = (isMember.not() && viewModel.groupMembers.size >= MAX_ADDABLE_PEOPLE_IN_GROUP).not()
)
}
)
}
}
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M19 17V19H7V17S7 13 13 13 19 17 19 17M16 8A3 3 0 1 0 13 11A3 3 0 0 0 16 8M19.2 13.06A5.6 5.6 0 0 1 21 17V19H24V17S24 13.55 19.2 13.06M18 5A2.91 2.91 0 0 0 17.11 5.14A5 5 0 0 1 17.11 10.86A2.91 2.91 0 0 0 18 11A3 3 0 0 0 18 5M8 10H5V7H3V10H0V12H3V15H5V12H8Z" />
</vector>

View File

@ -129,6 +129,13 @@
<string name="friends_all">All</string>
<string name="friends_blocked">Blocked</string>
<string name="friends_deny_all_incoming">Clear all incoming requests</string>
<string name="frends_new_group">New Group</string>
<string name="create_group">Create a new group</string>
<string name="create_group_description">Round up your crew in a group chat. You can add up to %1$d people.</string>
<string name="create_group_name">Group Name</string>
<string name="create_group_search">Search Friends</string>
<string name="create_group_action">Create</string>
<string name="server_plus_alt">Add server</string>
<string name="discover_alt">Discover</string>