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.AboutScreen
|
||||||
import chat.revolt.screens.about.AttributionScreen
|
import chat.revolt.screens.about.AttributionScreen
|
||||||
import chat.revolt.screens.chat.ChatRouterScreen
|
import chat.revolt.screens.chat.ChatRouterScreen
|
||||||
|
import chat.revolt.screens.create.CreateGroupScreen
|
||||||
import chat.revolt.screens.labs.LabsRootScreen
|
import chat.revolt.screens.labs.LabsRootScreen
|
||||||
import chat.revolt.screens.login.LoginGreetingScreen
|
import chat.revolt.screens.login.LoginGreetingScreen
|
||||||
import chat.revolt.screens.login.LoginScreen
|
import chat.revolt.screens.login.LoginScreen
|
||||||
|
|
@ -365,6 +366,8 @@ fun AppEntrypoint(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable("create/group") { CreateGroupScreen(navController) }
|
||||||
|
|
||||||
composable("discover") { DiscoverScreen(navController) }
|
composable("discover") { DiscoverScreen(navController) }
|
||||||
|
|
||||||
composable("settings") { SettingsScreen(navController) }
|
composable("settings") { SettingsScreen(navController) }
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ object FriendRequests {
|
||||||
|
|
||||||
fun getFriends(excludeOnline: Boolean = false): List<User> {
|
fun getFriends(excludeOnline: Boolean = false): List<User> {
|
||||||
return RevoltAPI.userCache.values.filter { 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?,
|
serverId: String?,
|
||||||
userId: String,
|
userId: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val highestColourRole = serverId?.let {
|
val highestColourRole = serverId?.let {
|
||||||
user?.id?.let { userId ->
|
user?.id?.let { userId ->
|
||||||
|
|
@ -79,5 +80,6 @@ fun MemberListItem(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
trailingContent = trailingContent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1038,6 +1038,7 @@ fun ChannelNavigator(
|
||||||
toggleDrawer()
|
toggleDrawer()
|
||||||
}
|
}
|
||||||
FriendsScreen(
|
FriendsScreen(
|
||||||
|
topNav = topNav,
|
||||||
useDrawer = useDrawer,
|
useDrawer = useDrawer,
|
||||||
onDrawerClicked = toggleDrawer,
|
onDrawerClicked = toggleDrawer,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
import chat.revolt.api.internals.FriendRequests
|
import chat.revolt.api.internals.FriendRequests
|
||||||
import chat.revolt.api.routes.user.unfriendUser
|
import chat.revolt.api.routes.user.unfriendUser
|
||||||
|
|
@ -40,7 +42,7 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FriendsScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) {
|
fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: () -> Unit) {
|
||||||
var overflowMenuShown by remember { mutableStateOf(false) }
|
var overflowMenuShown by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
@ -67,6 +69,14 @@ fun FriendsScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
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 = {
|
IconButton(onClick = {
|
||||||
overflowMenuShown = true
|
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_all">All</string>
|
||||||
<string name="friends_blocked">Blocked</string>
|
<string name="friends_blocked">Blocked</string>
|
||||||
<string name="friends_deny_all_incoming">Clear all incoming requests</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="server_plus_alt">Add server</string>
|
||||||
<string name="discover_alt">Discover</string>
|
<string name="discover_alt">Discover</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue