diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 8c8229483c..fad7fc0d46 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -524,6 +524,9 @@ class GroupInfo ( override val fullName get() = groupProfile.fullName override val image get() = groupProfile.image + val canEdit: Boolean + get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent + val canDelete: Boolean get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrent @@ -1364,6 +1367,7 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent() @Serializable @SerialName("userDeleted") class UserDeleted(): RcvGroupEvent() @Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent() + @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent() val text: String get() = when (this) { is MemberAdded -> String.format(generalGetString(R.string.rcv_group_event_member_added), profile.profileViewName) @@ -1372,6 +1376,7 @@ sealed class RcvGroupEvent() { is MemberDeleted -> String.format(generalGetString(R.string.rcv_group_event_member_deleted), profile.profileViewName) is UserDeleted -> generalGetString(R.string.rcv_group_event_user_deleted) is GroupDeleted -> generalGetString(R.string.rcv_group_event_group_deleted) + is GroupUpdated -> generalGetString(R.string.rcv_group_event_updated_group_profile) } } @@ -1379,9 +1384,11 @@ sealed class RcvGroupEvent() { sealed class SndGroupEvent() { @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() @Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent() + @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent() val text: String get() = when (this) { is MemberDeleted -> String.format(generalGetString(R.string.snd_group_event_member_deleted), profile.profileViewName) is UserLeft -> generalGetString(R.string.snd_group_event_user_left) + is GroupUpdated -> generalGetString(R.string.snd_group_event_group_profile_updated) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index e08e97329d..d0d3c7fe0b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -620,6 +620,24 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager return emptyList() } + suspend fun apiUpdateGroup(groupId: Long, groupProfile: GroupProfile): GroupInfo? { + return when (val r = sendCmd(CC.ApiUpdateGroupProfile(groupId, groupProfile))) { + is CR.GroupUpdated -> r.toGroup + is CR.ChatCmdError -> { + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_saving_group_profile), "$r.chatError") + null + } + else -> { + Log.e(TAG, "apiUpdateGroup bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg( + generalGetString(R.string.error_saving_group_profile), + "${r.responseType}: ${r.details}" + ) + null + } + } + } + fun apiErrorAlert(method: String, title: String, r: CR) { val errMsg = "${r.responseType}: ${r.details}" Log.e(TAG, "$method bad response: $errMsg") @@ -716,6 +734,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager chatModel.updateGroup(r.groupInfo) is CR.DeletedMemberUser -> chatModel.updateGroup(r.groupInfo) + is CR.GroupUpdated -> + chatModel.updateGroup(r.toGroup) is CR.RcvFileStart -> chatItemSimpleUpdate(r.chatItem) is CR.RcvFileComplete -> @@ -1028,6 +1048,7 @@ sealed class CC { class ApiRemoveMember(val groupId: Long, val memberId: Long): CC() class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() + class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() class GetUserSMPServers: CC() class SetUserSMPServers(val smpServers: List): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() @@ -1077,6 +1098,7 @@ sealed class CC { is ApiRemoveMember -> "/_remove #$groupId $memberId" is ApiLeaveGroup -> "/_leave #$groupId" is ApiListMembers -> "/_members #$groupId" + is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}" is GetUserSMPServers -> "/smp_servers" is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" @@ -1127,6 +1149,7 @@ sealed class CC { is ApiRemoveMember -> "apiRemoveMember" is ApiLeaveGroup -> "apiLeaveGroup" is ApiListMembers -> "apiListMembers" + is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" is GetUserSMPServers -> "getUserSMPServers" is SetUserSMPServers -> "setUserSMPServers" is APISetNetworkConfig -> "/apiSetNetworkConfig" @@ -1265,6 +1288,7 @@ sealed class CR { @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupUpdated") class GroupUpdated(val toGroup: GroupInfo): CR() // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileStart") class RcvFileStart(val chatItem: AChatItem): CR() @@ -1349,6 +1373,7 @@ sealed class CR { is JoinedGroupMember -> "joinedGroupMember" is ConnectedToGroupMember -> "connectedToGroupMember" is GroupRemoved -> "groupRemoved" + is GroupUpdated -> "groupUpdated" is RcvFileAccepted -> "rcvFileAccepted" is RcvFileStart -> "rcvFileStart" is RcvFileComplete -> "rcvFileComplete" @@ -1432,6 +1457,7 @@ sealed class CR { is JoinedGroupMember -> "groupInfo: $groupInfo\nmember: $member" is ConnectedToGroupMember -> "groupInfo: $groupInfo\nmember: $member" is GroupRemoved -> json.encodeToString(groupInfo) + is GroupUpdated -> json.encodeToString(toGroup) is RcvFileAccepted -> json.encodeToString(chatItem) is RcvFileStart -> json.encodeToString(chatItem) is RcvFileComplete -> json.encodeToString(chatItem) diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index 4fb1673df6..8f02fee808 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -18,6 +18,7 @@ val MessagePreviewLight = Color(49, 45, 44, 255) val ToolbarLight = Color(220, 220, 220, 20) val ToolbarDark = Color(80, 80, 80, 20) val SettingsBackgroundLight = Color(220, 216, 215, 90) +val SettingsSecondaryLight = Color(200, 196, 195, 90) val GroupDark = Color(80, 80, 80, 60) val IncomingCallLight = Color(239, 237, 236, 255) val IncomingCallDark = Color(34, 30, 29, 255) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 49aa3fe3bb..74de681ae4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -103,7 +103,6 @@ fun ChatInfoLayout( SectionItemView { NetworkStatusRow(chat.serverInfo.networkStatus) } - val rcvServers = connStats.rcvServers if (rcvServers != null && rcvServers.isNotEmpty()) { SectionDivider() @@ -144,7 +143,7 @@ fun ChatInfoHeader(cInfo: ChatInfo) { Modifier.padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - ChatInfoImage(cInfo, size = 192.dp, iconColor = HighOrLowlight) + ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight) Text( cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colors.onBackground, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 30e45a4edb..ffb3f282bd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -112,7 +112,7 @@ fun ChatView(chatModel: ChatModel) { close = close, modifier = Modifier, background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - GroupChatInfoView(cInfo.groupInfo, chatModel, close) + GroupChatInfoView(chatModel, close) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt index 282ffa7c71..f189cb5617 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt @@ -87,7 +87,11 @@ fun AddGroupMembersLayout( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - ChatInfoToolbarTitle(ChatInfo.Group(groupInfo), imageSize = 60.dp, iconColor = HighOrLowlight) // TODO tertiary color + ChatInfoToolbarTitle( + ChatInfo.Group(groupInfo), + imageSize = 60.dp, + iconColor = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight + ) } SectionSpacer() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 3106f08aaa..9931455cae 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -28,10 +28,11 @@ import chat.simplex.app.views.chatlist.populateGroupMembers import chat.simplex.app.views.helpers.* @Composable -fun GroupChatInfoView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { +fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } - if (chat != null) { + if (chat != null && chat.chatInfo is ChatInfo.Group) { + val groupInfo = chat.chatInfo.groupInfo GroupChatInfoLayout( chat, groupInfo, @@ -62,6 +63,9 @@ fun GroupChatInfoView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> U } } }, + editGroupProfile = { + ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) } + }, deleteGroup = { deleteGroupDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) } @@ -109,6 +113,7 @@ fun GroupChatInfoLayout( members: List, addMembers: () -> Unit, showMemberInfo: (GroupMember) -> Unit, + editGroupProfile: () -> Unit, deleteGroup: () -> Unit, clearChat: () -> Unit, leaveGroup: () -> Unit, @@ -143,6 +148,12 @@ fun GroupChatInfoLayout( SectionSpacer() SectionView { + if (groupInfo.canEdit) { + SectionItemView { + EditGroupProfileButton(editGroupProfile) + } + SectionDivider() + } SectionItemView { ClearChatButton(clearChat) } @@ -235,6 +246,24 @@ fun MemberRow(member: GroupMember, showMemberInfo: ((GroupMember) -> Unit)? = nu } } +@Composable +fun EditGroupProfileButton(editGroupProfile: () -> Unit) { + Row( + Modifier + .fillMaxSize() + .clickable { editGroupProfile() }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Outlined.Edit, + stringResource(R.string.button_edit_group_profile), + tint = MaterialTheme.colors.primary + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.button_edit_group_profile), color = MaterialTheme.colors.primary) + } +} + @Composable fun LeaveGroupButton(leaveGroup: () -> Unit) { Row( @@ -283,7 +312,7 @@ fun PreviewGroupChatInfoLayout() { ), groupInfo = GroupInfo.sampleData, members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), - addMembers = {}, showMemberInfo = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {} + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {} ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index 1864c0a3aa..6fbbc7d1bd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -131,7 +131,7 @@ fun GroupMemberInfoHeader(member: GroupMember) { Modifier.padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - ProfileImage(size = 192.dp, member.image, color = HighOrLowlight) + ProfileImage(size = 192.dp, member.image, color = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight) Text( member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colors.onBackground, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt new file mode 100644 index 0000000000..52995d30a8 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt @@ -0,0 +1,173 @@ +package chat.simplex.app.views.chat.group + +import android.content.res.Configuration +import android.graphics.Bitmap +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.ProfileNameField +import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.isValidDisplayName +import chat.simplex.app.views.usersettings.* +import com.google.accompanist.insets.ProvideWindowInsets +import com.google.accompanist.insets.navigationBarsWithImePadding +import kotlinx.coroutines.launch + +@Composable +fun GroupProfileView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { + GroupProfileLayout( + close = close, + groupProfile = groupInfo.groupProfile, + saveProfile = { p -> + withApi { + val gInfo = chatModel.controller.apiUpdateGroup(groupInfo.groupId, p) + if (gInfo != null) { + chatModel.updateGroup(gInfo) + close.invoke() + } + } + } + ) +} + +@Composable +fun GroupProfileLayout( + close: () -> Unit, + groupProfile: GroupProfile, + saveProfile: (GroupProfile) -> Unit, +) { + val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val displayName = remember { mutableStateOf(groupProfile.displayName) } + val fullName = remember { mutableStateOf(groupProfile.fullName) } + val chosenImage = remember { mutableStateOf(null) } + val profileImage = remember { mutableStateOf(groupProfile.image) } + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val focusRequester = remember { FocusRequester() } + + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + ModalBottomSheetLayout( + scrimColor = Color.Black.copy(alpha = 0.12F), + modifier = Modifier.navigationBarsWithImePadding(), + sheetContent = { + GetImageBottomSheet( + chosenImage, + onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) }, + hideBottomSheet = { + scope.launch { bottomSheetModalState.hide() } + }) + }, + sheetState = bottomSheetModalState, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) { + ModalView(close = close) { + Column( + Modifier + .verticalScroll(scrollState) + .padding(bottom = 16.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + stringResource(R.string.group_profile_is_stored_on_members_devices), + Modifier.padding(bottom = 24.dp), + color = MaterialTheme.colors.onBackground, + lineHeight = 22.sp + ) + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Box( + Modifier + .fillMaxWidth() + .padding(bottom = 24.dp), + contentAlignment = Alignment.Center + ) { + Box(contentAlignment = Alignment.TopEnd) { + Box(contentAlignment = Alignment.Center) { + ProfileImage(192.dp, profileImage.value) + EditImageButton { scope.launch { bottomSheetModalState.show() } } + } + if (profileImage.value != null) { + DeleteImageButton { profileImage.value = null } + } + } + } + Text( + stringResource(R.string.group_display_name_field), + Modifier.padding(bottom = 3.dp) + ) + ProfileNameField(displayName, focusRequester) + val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else "" + Text( + errorText, + fontSize = 15.sp, + color = MaterialTheme.colors.error + ) + Spacer(Modifier.height(3.dp)) + Text( + stringResource(R.string.group_full_name_field), + Modifier.padding(bottom = 5.dp) + ) + ProfileNameField(fullName) + Spacer(Modifier.height(16.dp)) + Row { + TextButton(stringResource(R.string.cancel_verb)) { + close.invoke() + } + Spacer(Modifier.padding(horizontal = 8.dp)) + val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) + if (enabled) { + Text( + stringResource(R.string.save_group_profile), + modifier = Modifier.clickable { saveProfile(GroupProfile(displayName.value, fullName.value, profileImage.value)) }, + color = MaterialTheme.colors.primary + ) + } else { + Text( + stringResource(R.string.save_group_profile), + color = HighOrLowlight + ) + } + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + } + } + } + } +} + +@Preview(showBackground = true) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +) +@Composable +fun PreviewGroupProfileLayout() { + SimpleXTheme { + GroupProfileLayout( + close = {}, + groupProfile = GroupProfile.sampleData, + saveProfile = { _ -> } + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt index cc887ccb65..73e2c7fa0f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt @@ -115,8 +115,7 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { } Text( stringResource(R.string.group_display_name_field), - style = MaterialTheme.typography.h6, - modifier = Modifier.padding(bottom = 3.dp) + Modifier.padding(bottom = 3.dp) ) ProfileNameField(displayName, focusRequester) val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else "" @@ -128,16 +127,16 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { Spacer(Modifier.height(3.dp)) Text( stringResource(R.string.group_full_name_field), - style = MaterialTheme.typography.h6, - modifier = Modifier.padding(bottom = 5.dp) + Modifier.padding(bottom = 5.dp) ) ProfileNameField(fullName) Spacer(Modifier.height(8.dp)) val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) if (enabled) { - val groupProfile = GroupProfile(displayName.value, fullName.value, profileImage.value) - CreateGroupButton(MaterialTheme.colors.primary, Modifier.clickable { createGroup(groupProfile) }.padding(8.dp)) + CreateGroupButton(MaterialTheme.colors.primary, Modifier + .clickable { createGroup(GroupProfile(displayName.value, fullName.value, profileImage.value)) } + .padding(8.dp)) } else { CreateGroupButton(HighOrLowlight, Modifier.padding(8.dp)) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt index 967ec73f2f..97b895f8fc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt @@ -200,7 +200,7 @@ fun UserProfileLayout( } @Composable -private fun ProfileNameTextField(name: MutableState) { +fun ProfileNameTextField(name: MutableState) { BasicTextField( value = name.value, onValueChange = { name.value = it }, @@ -218,7 +218,7 @@ private fun ProfileNameTextField(name: MutableState) { } @Composable -private fun ProfileNameRow(label: String, text: String) { +fun ProfileNameRow(label: String, text: String) { Row(Modifier.padding(bottom = 24.dp)) { Text( label, @@ -234,7 +234,7 @@ private fun ProfileNameRow(label: String, text: String) { } @Composable -private fun TextButton(text: String, click: () -> Unit) { +fun TextButton(text: String, click: () -> Unit) { Text( text, color = MaterialTheme.colors.primary, diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index f98fc9066c..84025ac04c 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -532,8 +532,10 @@ удалил(а) %1$s удалил(а) вас из группы удалил(а) группу + обновил(а) профиль группы вы удалили %1$s вы покинули группу + профиль группы обновлен участник @@ -572,6 +574,7 @@ Удалить группу? Группа будет удалена для всех участников - это действие нельзя отменить! Выйти из группы + Редактировать профиль группы ДЛЯ КОНСОЛИ @@ -598,4 +601,9 @@ Группа полностью децентрализована — она видна только участникам. Имя группы: Полное имя: + + + Профиль группы хранится на устройствах участников, а не на серверах. + Сохранить профиль группы + Ошибка при сохранении профиля группы diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 6c8be15b20..8bf865620f 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -534,8 +534,10 @@ removed %1$s removed you deleted group + updated group profile you removed %1$s you left + group profile updated member @@ -574,6 +576,7 @@ Delete group? Group will be deleted for all members - this cannot be undone! Leave group + Edit group profile FOR CONSOLE @@ -600,4 +603,9 @@ The group is fully decentralized – it is visible only to the members. Group display name: Group full name: + + + Group profile is stored on members\' devices, not on the servers. + Save group profile + Error saving group profile diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 99d93372ac..08182e08c2 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -823,6 +823,8 @@ func processReceivedMsg(_ res: ChatResponse) async { m.updateGroup(groupInfo) case let .deletedMemberUser(groupInfo, _): m.updateGroup(groupInfo) + case let .groupUpdated(toGroup): + m.updateGroup(toGroup) case let .rcvFileStart(aChatItem): chatItemSimpleUpdate(aChatItem) case let .rcvFileComplete(aChatItem):