feat: group create screen
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
b746c1d47e
commit
a5739f0f58
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -1038,6 +1038,7 @@ fun ChannelNavigator(
|
|||
toggleDrawer()
|
||||
}
|
||||
FriendsScreen(
|
||||
topNav = topNav,
|
||||
useDrawer = useDrawer,
|
||||
onDrawerClicked = toggleDrawer,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue