feat: Enhance group chat functionality and UI improvements

- Added support for group action system messages in MessageBubble.
- Implemented group invite handling with inline cards for joining groups.
- Updated MessageBubble to display group sender labels and admin badges.
- Enhanced image decryption logic for group attachments.
- Modified BlurredAvatarBackground to load system avatars based on public keys.
- Improved SwipeBackContainer with layer management for better swipe effects.
- Updated VerifiedBadge to use dynamic icons based on user verification status.
- Added new drawable resource for admin badge icon.
This commit is contained in:
2026-03-01 00:01:01 +05:00
parent 3f2b52b578
commit a0569648e8
28 changed files with 5053 additions and 483 deletions

View File

@@ -42,6 +42,8 @@ import com.rosetta.messenger.ui.auth.DeviceConfirmScreen
import com.rosetta.messenger.ui.chats.ChatDetailScreen
import com.rosetta.messenger.ui.chats.ChatsListScreen
import com.rosetta.messenger.ui.chats.ConnectionLogsScreen
import com.rosetta.messenger.ui.chats.GroupInfoScreen
import com.rosetta.messenger.ui.chats.GroupSetupScreen
import com.rosetta.messenger.ui.chats.RequestsListScreen
import com.rosetta.messenger.ui.chats.SearchScreen
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
@@ -498,6 +500,8 @@ sealed class Screen {
data object Profile : Screen()
data object Requests : Screen()
data object Search : Screen()
data object GroupSetup : Screen()
data class GroupInfo(val group: SearchUser) : Screen()
data class ChatDetail(val user: SearchUser) : Screen()
data class OtherProfile(val user: SearchUser) : Screen()
data object Updates : Screen()
@@ -600,10 +604,15 @@ fun MainScreen(
val isProfileVisible by remember { derivedStateOf { navStack.any { it is Screen.Profile } } }
val isRequestsVisible by remember { derivedStateOf { navStack.any { it is Screen.Requests } } }
val isSearchVisible by remember { derivedStateOf { navStack.any { it is Screen.Search } } }
val isGroupSetupVisible by remember { derivedStateOf { navStack.any { it is Screen.GroupSetup } } }
val chatDetailScreen by remember {
derivedStateOf { navStack.filterIsInstance<Screen.ChatDetail>().lastOrNull() }
}
val selectedUser = chatDetailScreen?.user
val groupInfoScreen by remember {
derivedStateOf { navStack.filterIsInstance<Screen.GroupInfo>().lastOrNull() }
}
val selectedGroup = groupInfoScreen?.group
val otherProfileScreen by remember {
derivedStateOf { navStack.filterIsInstance<Screen.OtherProfile>().lastOrNull() }
}
@@ -650,7 +659,10 @@ fun MainScreen(
}
}
fun popChatAndChildren() {
navStack = navStack.filterNot { it is Screen.ChatDetail || it is Screen.OtherProfile }
navStack =
navStack.filterNot {
it is Screen.ChatDetail || it is Screen.OtherProfile || it is Screen.GroupInfo
}
}
// ProfileViewModel для логов
@@ -689,7 +701,7 @@ fun MainScreen(
// 🔥 Простая навигация с swipe back
Box(modifier = Modifier.fillMaxSize()) {
// Base layer - chats list (всегда видимый, чтобы его было видно при свайпе)
SwipeBackBackgroundEffect(modifier = Modifier.fillMaxSize()) {
SwipeBackBackgroundEffect(modifier = Modifier.fillMaxSize(), layer = 0) {
ChatsListScreen(
isDarkTheme = isDarkTheme,
accountName = accountName,
@@ -702,7 +714,7 @@ fun MainScreen(
onToggleTheme = onToggleTheme,
onProfileClick = { pushScreen(Screen.Profile) },
onNewGroupClick = {
// TODO: Navigate to new group
pushScreen(Screen.GroupSetup)
},
onContactsClick = {
// TODO: Navigate to contacts
@@ -754,7 +766,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isRequestsVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Requests } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 1
) {
RequestsListScreen(
isDarkTheme = isDarkTheme,
@@ -783,7 +796,9 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isProfileVisible,
onBack = { popProfileAndChildren() },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 1,
propagateBackgroundProgress = false
) {
// Экран профиля
ProfileScreen(
@@ -816,7 +831,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isSafetyVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Safety } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 2
) {
SafetyScreen(
isDarkTheme = isDarkTheme,
@@ -833,7 +849,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isBackupVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Backup } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 3
) {
BackupScreen(
isDarkTheme = isDarkTheme,
@@ -878,7 +895,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isThemeVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Theme } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 2
) {
ThemeScreen(
isDarkTheme = isDarkTheme,
@@ -891,7 +909,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isAppearanceVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Appearance } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 2
) {
com.rosetta.messenger.ui.settings.AppearanceScreen(
isDarkTheme = isDarkTheme,
@@ -912,7 +931,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isUpdatesVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Updates } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 2
) {
UpdatesScreen(
isDarkTheme = isDarkTheme,
@@ -938,6 +958,7 @@ fun MainScreen(
isVisible = selectedUser != null,
onBack = { popChatAndChildren() },
isDarkTheme = isDarkTheme,
layer = 1,
swipeEnabled = !isChatSwipeLocked,
propagateBackgroundProgress = false
) {
@@ -959,6 +980,9 @@ fun MainScreen(
pushScreen(Screen.OtherProfile(user))
}
},
onGroupInfoClick = { groupUser ->
pushScreen(Screen.GroupInfo(groupUser))
},
onNavigateToChat = { forwardUser ->
// 📨 Forward: переход в выбранный чат с полными данными
navStack =
@@ -973,10 +997,54 @@ fun MainScreen(
}
}
var isGroupInfoSwipeEnabled by remember { mutableStateOf(true) }
LaunchedEffect(selectedGroup?.publicKey) {
isGroupInfoSwipeEnabled = true
}
SwipeBackContainer(
isVisible = selectedGroup != null,
onBack = { navStack = navStack.filterNot { it is Screen.GroupInfo } },
isDarkTheme = isDarkTheme,
layer = 2,
swipeEnabled = isGroupInfoSwipeEnabled,
propagateBackgroundProgress = false
) {
selectedGroup?.let { groupUser ->
GroupInfoScreen(
groupUser = groupUser,
currentUserPublicKey = accountPublicKey,
currentUserPrivateKey = accountPrivateKey,
isDarkTheme = isDarkTheme,
avatarRepository = avatarRepository,
onBack = { navStack = navStack.filterNot { it is Screen.GroupInfo } },
onMemberClick = { member ->
if (member.publicKey == accountPublicKey) {
pushScreen(Screen.Profile)
} else {
pushScreen(Screen.OtherProfile(member))
}
},
onSwipeBackEnabledChanged = { enabled ->
isGroupInfoSwipeEnabled = enabled
},
onGroupLeft = {
navStack =
navStack.filterNot {
it is Screen.GroupInfo ||
(it is Screen.ChatDetail &&
it.user.publicKey == groupUser.publicKey)
}
}
)
}
}
SwipeBackContainer(
isVisible = isSearchVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Search } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 1
) {
// Экран поиска
SearchScreen(
@@ -996,10 +1064,30 @@ fun MainScreen(
)
}
SwipeBackContainer(
isVisible = isGroupSetupVisible,
onBack = { navStack = navStack.filterNot { it is Screen.GroupSetup } },
isDarkTheme = isDarkTheme,
layer = 1
) {
GroupSetupScreen(
isDarkTheme = isDarkTheme,
accountPublicKey = accountPublicKey,
accountPrivateKey = accountPrivateKey,
onBack = { navStack = navStack.filterNot { it is Screen.GroupSetup } },
onGroupOpened = { groupUser ->
navStack =
navStack.filterNot { it is Screen.GroupSetup } +
Screen.ChatDetail(groupUser)
}
)
}
SwipeBackContainer(
isVisible = isLogsVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Logs } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 2
) {
com.rosetta.messenger.ui.settings.ProfileLogsScreen(
isDarkTheme = isDarkTheme,
@@ -1012,7 +1100,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isCrashLogsVisible,
onBack = { navStack = navStack.filterNot { it is Screen.CrashLogs } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 1
) {
CrashLogsScreen(
onBackClick = { navStack = navStack.filterNot { it is Screen.CrashLogs } }
@@ -1022,7 +1111,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isConnectionLogsVisible,
onBack = { navStack = navStack.filterNot { it is Screen.ConnectionLogs } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 1
) {
ConnectionLogsScreen(
isDarkTheme = isDarkTheme,
@@ -1039,7 +1129,9 @@ fun MainScreen(
isVisible = selectedOtherUser != null,
onBack = { navStack = navStack.filterNot { it is Screen.OtherProfile } },
isDarkTheme = isDarkTheme,
swipeEnabled = isOtherProfileSwipeEnabled
layer = 2,
swipeEnabled = isOtherProfileSwipeEnabled,
propagateBackgroundProgress = false
) {
selectedOtherUser?.let { currentOtherUser ->
OtherProfileScreen(
@@ -1066,7 +1158,8 @@ fun MainScreen(
SwipeBackContainer(
isVisible = isBiometricVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Biometric } },
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
layer = 2
) {
val biometricManager = remember {
com.rosetta.messenger.biometric.BiometricAuthManager(context)