feat: Add ability to add members to groups from Channel Info
This commit introduces a new bottom sheet that allows users to add or remove members from a group directly from the channel info screen. - A new "Add Members" button is now available in the `ChannelInfoSheet` for group chats. - Tapping this button opens the `AddMemberToGroupSheet`, which lists all friends. - Users can search for friends and add or remove them from the group. - The "Manage Notifications" button is now hidden for group chats. Additionally, this commit includes the following changes: - **Changelog**: The changelog fetching and display functionality has been temporarily disabled. - **Splash Screen**: The splash screen icon has been updated and now uses an inset drawable (`splash_screen_icon_square.xml`) to adjust its padding, which also resolves an icon display issue on Android 13. - **Build**: All build variants are now signed with the debug key to simplify development builds.
|
|
@ -100,6 +100,7 @@ android {
|
|||
"FLAVOUR_ID",
|
||||
"\"${buildproperty("build.flavour_id", "RVX_BUILD_FLAVOUR_ID")}\""
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
debug {
|
||||
|
|
@ -110,7 +111,7 @@ android {
|
|||
resValue(
|
||||
"string",
|
||||
"app_name",
|
||||
buildproperty("build.debug.app_name", "RVX_DEBUG_APP_NAME")!!
|
||||
buildproperty("build.debug.app_name", "RVX_DEBUG_APP_NAME") ?: "ZekoChat Debug"
|
||||
)
|
||||
|
||||
// buildConfigField(
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
|
|||
cachedIndex =
|
||||
PeptideJson.decodeFromString(ChangelogIndex.serializer(), response.bodyAsText())
|
||||
return cachedIndex as ChangelogIndex
|
||||
} catch (e: Error) {
|
||||
} catch (_: Error) {
|
||||
return ChangelogIndex()
|
||||
}catch (_: Exception) {
|
||||
return ChangelogIndex()
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +97,8 @@ class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
|
|||
}
|
||||
|
||||
suspend fun getLatestChangelogCode(): String {
|
||||
return getLatestChangelog().version.code.toString()
|
||||
// return getLatestChangelog().version.code.toString()
|
||||
return ""
|
||||
}
|
||||
|
||||
suspend fun hasSeenCurrent(): Boolean {
|
||||
|
|
@ -105,7 +108,7 @@ class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
|
|||
)
|
||||
}
|
||||
|
||||
val latest = getLatestChangelog().version.code
|
||||
// val latest = getLatestChangelog().version.code
|
||||
val lastRead = kvStorage.get("latestChangelogRead")
|
||||
|
||||
if (lastRead == null) {
|
||||
|
|
@ -113,7 +116,8 @@ class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
|
|||
}
|
||||
|
||||
// If the last read changelog is >= the latest, it has been read
|
||||
return lastRead.toLong() >= latest
|
||||
// return lastRead.toLong() >= latest
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun markAsSeen() {
|
||||
|
|
@ -124,7 +128,7 @@ class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
|
|||
}
|
||||
|
||||
val index = fetchChangelogIndex()
|
||||
val latest = index.changelogs.maxByOrNull { it.version.code }!!.version.code.toString()
|
||||
kvStorage.set("latestChangelogRead", latest)
|
||||
// val latest = index.changelogs.maxByOrNull { it.version.code }!!.version.code.toString()
|
||||
// kvStorage.set("latestChangelogRead", latest)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,8 +187,8 @@ class ChatRouterViewModel @Inject constructor(
|
|||
|
||||
latestChangelogRead = changelogs.hasSeenCurrent()
|
||||
latestChangelog = changelogs.getLatestChangelogCode()
|
||||
latestChangelogBody =
|
||||
changelogs.fetchChangelogByVersionCode(latestChangelog.toLong()).rendered
|
||||
// latestChangelogBody =
|
||||
// changelogs.fetchChangelogByVersionCode(latestChangelog.toLong()).rendered
|
||||
if (!latestChangelogRead) {
|
||||
changelogs.markAsSeen()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,12 +60,12 @@ class ChangelogsSettingsScreenViewModel @Inject constructor(
|
|||
var renderedChangelog by mutableStateOf("")
|
||||
|
||||
suspend fun requestChangelog(version: String) {
|
||||
viewModelScope.launch {
|
||||
renderedChangelog = Changelogs(
|
||||
context,
|
||||
kvStorage
|
||||
).fetchChangelogByVersionCode(version.toLong()).rendered
|
||||
}
|
||||
// viewModelScope.launch {
|
||||
// renderedChangelog = Changelogs(
|
||||
// context,
|
||||
// kvStorage
|
||||
// ).fetchChangelogByVersionCode(version.toLong()).rendered
|
||||
// }
|
||||
}
|
||||
|
||||
suspend fun populate() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
package chat.zekochat.sheets
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import chat.zekochat.api.PeptideAPI
|
||||
import chat.zekochat.api.PeptideHttp
|
||||
import chat.zekochat.api.PeptideJson
|
||||
import chat.zekochat.api.api
|
||||
import chat.zekochat.api.internals.FriendRequests
|
||||
import chat.zekochat.api.routes.channel.addMember
|
||||
import chat.zekochat.api.routes.channel.removeMember
|
||||
import chat.zekochat.api.schemas.User
|
||||
import chat.zekochat.composables.chat.MemberListItem
|
||||
import chat.zekochat.screens.create.MAX_ADDABLE_PEOPLE_IN_GROUP
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
|
||||
class AddMemberToGroupSheetViewModel : ViewModel() {
|
||||
var groupMembers = mutableStateListOf<String>()
|
||||
var friendSearchQuery by mutableStateOf("")
|
||||
var loadingUser by mutableStateOf("")
|
||||
var friendsFilteredBySearch = mutableStateListOf<String>()
|
||||
var error by mutableStateOf<String?>(null)
|
||||
|
||||
suspend fun fetchGroupParticipants(channelId: String) {
|
||||
val response = PeptideHttp.get("/channels/$channelId/members".api())
|
||||
.bodyAsText()
|
||||
|
||||
val users = PeptideJson.decodeFromString(
|
||||
ListSerializer(User.serializer()),
|
||||
response
|
||||
).mapNotNull { it.id }
|
||||
|
||||
groupMembers.clear()
|
||||
groupMembers.addAll(users)
|
||||
|
||||
}
|
||||
|
||||
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 addMemberToGroup(channelId: String, 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 {
|
||||
groupMembers.map {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
addMember(channelId, it)
|
||||
}
|
||||
}
|
||||
popBackStack()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
fun addMemberToGroup(channelId: String, userId: String) {
|
||||
|
||||
if(loadingUser.isNotEmpty()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
loadingUser = userId
|
||||
error = null
|
||||
|
||||
addMember(channelId, userId)
|
||||
groupMembers.add(userId)
|
||||
|
||||
loadingUser = ""
|
||||
} catch (e: Exception) {
|
||||
loadingUser = ""
|
||||
error = e.message
|
||||
} catch (e: Error) {
|
||||
loadingUser = ""
|
||||
groupMembers.add(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMemberFromGroup(channelId: String, userId: String) {
|
||||
|
||||
if(loadingUser.isNotEmpty()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
loadingUser = userId
|
||||
error = null
|
||||
|
||||
removeMember(channelId, userId)
|
||||
groupMembers.remove(userId)
|
||||
|
||||
loadingUser = ""
|
||||
} catch (e: Exception) {
|
||||
loadingUser = ""
|
||||
error = e.message
|
||||
} catch (e: Error) {
|
||||
loadingUser = ""
|
||||
groupMembers.remove(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddMemberToGroupSheet(
|
||||
channelId: String,
|
||||
viewModel: AddMemberToGroupSheetViewModel = viewModel(),
|
||||
onHideSheet: suspend () -> Unit
|
||||
)
|
||||
{
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.filterFriends()
|
||||
viewModel.fetchGroupParticipants(channelId);
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.imePadding()
|
||||
)
|
||||
{
|
||||
AnimatedVisibility(visible = viewModel.error?.isNotBlank() ?: false) {
|
||||
Text(
|
||||
text = viewModel.error ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(contentPadding = PaddingValues(bottom = 78.0.dp)) {
|
||||
items(viewModel.friendsFilteredBySearch.size) { index ->
|
||||
val friend = PeptideAPI.userCache[viewModel.friendsFilteredBySearch[index]]
|
||||
?: return@items
|
||||
val isMember = viewModel.groupMembers.contains(friend.id)
|
||||
val isLoading = viewModel.loadingUser == friend.id
|
||||
|
||||
MemberListItem(
|
||||
member = null,
|
||||
user = friend,
|
||||
serverId = null,
|
||||
userId = friend.id!!,
|
||||
modifier = Modifier.clickable {
|
||||
if (isMember) {
|
||||
viewModel.removeMemberFromGroup(channelId, friend.id)
|
||||
} else {
|
||||
viewModel.addMemberToGroup(channelId, friend.id)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
if(isLoading){
|
||||
CircularProgressIndicator(Modifier.size(24.dp))
|
||||
}else{
|
||||
|
||||
Checkbox(
|
||||
checked = isMember,
|
||||
onCheckedChange = null,
|
||||
enabled = (isMember.not() && viewModel.groupMembers.size >= MAX_ADDABLE_PEOPLE_IN_GROUP).not()
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ import kotlinx.coroutines.launch
|
|||
fun ChannelInfoSheet(channelId: String, onHideSheet: suspend () -> Unit) {
|
||||
val channel = PeptideAPI.channelCache[channelId]
|
||||
var memberListSheetShown by remember { mutableStateOf(false) }
|
||||
var addMemberSheetShown by remember { mutableStateOf(false) }
|
||||
var inviteDialogShown by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val permissions by rememberChannelPermissions(channelId)
|
||||
|
|
@ -66,6 +67,24 @@ fun ChannelInfoSheet(channelId: String, onHideSheet: suspend () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
if (addMemberSheetShown) {
|
||||
val addMemberSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
sheetState = addMemberSheetState,
|
||||
onDismissRequest = {
|
||||
addMemberSheetShown = false
|
||||
}
|
||||
) {
|
||||
AddMemberToGroupSheet(
|
||||
channelId = channelId,
|
||||
onHideSheet = {
|
||||
addMemberSheetShown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (inviteDialogShown) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
|
|
@ -176,7 +195,9 @@ fun ChannelInfoSheet(channelId: String, onHideSheet: suspend () -> Unit) {
|
|||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = {}
|
||||
onClick = {
|
||||
addMemberSheetShown = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -184,20 +205,22 @@ fun ChannelInfoSheet(channelId: String, onHideSheet: suspend () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
SheetButton(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.channel_info_sheet_options_notifications_manage),
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.icn_notification_settings_24dp),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = {}
|
||||
)
|
||||
if(channel.channelType != ChannelType.Group){
|
||||
SheetButton(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.channel_info_sheet_options_notifications_manage),
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.icn_notification_settings_24dp),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
(permissions has PermissionBit.ManageChannel || permissions has PermissionBit.ManageRole)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/splash_screen_icon"
|
||||
android:insetLeft="48dp"
|
||||
android:insetRight="48dp"
|
||||
android:insetTop="40dp"
|
||||
android:insetBottom="40dp" />
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -5,6 +5,6 @@
|
|||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowSplashScreenBackground">@color/background</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_icon</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_icon_square</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -2,9 +2,8 @@
|
|||
<resources>
|
||||
|
||||
<style name="Theme.Peptide.Starting" parent="Theme.SplashScreen">
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_icon</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_icon_square</item>
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="android:windowSplashScreenBehavior">icon_preferred</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Peptide</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<!-- We temporarily don't use a dedicated splash screen theme due to Samsung specific issues.
|
||||
This theme is unused. -->
|
||||
<style name="Theme.Peptide.Starting" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/zeko_rounded_icon</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_screen_icon_square</item>
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Peptide</item>
|
||||
</style>
|
||||
|
|
|
|||