android, desktop: support business addresses and chats (#5302)

This commit is contained in:
spaced4ndy
2024-12-03 17:27:00 +04:00
committed by GitHub
parent 43fa4c43a2
commit a0b8cf62be
13 changed files with 155 additions and 49 deletions
@@ -1467,6 +1467,7 @@ data class GroupInfo (
val groupId: Long,
override val localDisplayName: String,
val groupProfile: GroupProfile,
val businessChat: BusinessChatInfo? = null,
val fullGroupPreferences: FullGroupPreferences,
val membership: GroupMember,
val hostConnCustomUserProfileId: Long? = null,
@@ -1497,7 +1498,7 @@ data class GroupInfo (
override val image get() = groupProfile.image
override val localAlias get() = ""
val canEdit: Boolean
val isOwner: Boolean
get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent
val canDelete: Boolean
@@ -1543,6 +1544,18 @@ data class GroupProfile (
}
}
@Serializable
data class BusinessChatInfo (
val memberId: String,
val chatType: BusinessChatType
)
@Serializable
enum class BusinessChatType {
@SerialName("business") Business,
@SerialName("customer") Customer,
}
@Serializable
data class GroupMember (
val groupMemberId: Long,
@@ -2526,6 +2526,14 @@ object ChatController {
}
}
}
is CR.BusinessLinkConnecting -> {
if (!active(r.user)) return
withChats {
updateGroup(rhId, r.groupInfo)
removeChat(rhId, r.fromContact.id)
}
}
is CR.JoinedGroupMemberConnecting ->
if (active(r.user)) {
withChats {
@@ -5484,6 +5492,7 @@ sealed class CR {
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR()
@Serializable @SerialName("groupLinkConnecting") class GroupLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember): CR()
@Serializable @SerialName("businessLinkConnecting") class BusinessLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val fromContact: Contact): CR()
@Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("groupMembers") class GroupMembers(val user: UserRef, val group: Group): CR()
@@ -5664,6 +5673,7 @@ sealed class CR {
is SentGroupInvitation -> "sentGroupInvitation"
is UserAcceptedGroupSent -> "userAcceptedGroupSent"
is GroupLinkConnecting -> "groupLinkConnecting"
is BusinessLinkConnecting -> "businessLinkConnecting"
is UserDeletedMember -> "userDeletedMember"
is LeftMemberUser -> "leftMemberUser"
is GroupMembers -> "groupMembers"
@@ -5837,6 +5847,7 @@ sealed class CR {
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
is UserAcceptedGroupSent -> json.encodeToString(groupInfo)
is GroupLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember")
is BusinessLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nfromContact: $fromContact")
is UserDeletedMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is LeftMemberUser -> withUser(user, json.encodeToString(groupInfo))
is GroupMembers -> withUser(user, json.encodeToString(group))
@@ -6108,11 +6119,16 @@ class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept?
}
@Serializable
class AutoAccept(val acceptIncognito: Boolean, val autoReply: MsgContent?) {
class AutoAccept(val businessAddress: Boolean, val acceptIncognito: Boolean, val autoReply: MsgContent?) {
companion object {
fun cmdString(autoAccept: AutoAccept?): String {
if (autoAccept == null) return "off"
val s = "on" + if (autoAccept.acceptIncognito) " incognito=on" else ""
var s = "on"
if (autoAccept.acceptIncognito) {
s += " incognito=on"
} else if (autoAccept.businessAddress) {
s += " business"
}
val msg = autoAccept.autoReply ?: return s
return s + " " + msg.cmdString
}
@@ -36,6 +36,7 @@ import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.launch
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
@@ -328,13 +329,14 @@ fun ModalData.GroupChatInfoLayout(
SectionSpacer()
SectionView {
if (groupInfo.canEdit) {
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
EditGroupProfileButton(editGroupProfile)
}
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) {
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
}
GroupPreferencesButton(openPreferences)
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
GroupPreferencesButton(prefsTitleId, openPreferences)
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
} else {
@@ -356,14 +358,21 @@ fun ModalData.GroupChatInfoLayout(
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
} else {
GroupLinkButton(manageGroupLink)
if (groupInfo.businessChat == null) {
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
} else {
GroupLinkButton(manageGroupLink)
}
}
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
AddMembersButton(tint, onAddMembersClick)
val addMembersTitleId = when (groupInfo.businessChat?.chatType) {
BusinessChatType.Customer -> MR.strings.button_add_team_members
BusinessChatType.Business -> MR.strings.button_add_friends
null -> MR.strings.button_add_members
}
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
}
if (members.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
@@ -439,10 +448,10 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
}
@Composable
private fun GroupPreferencesButton(onClick: () -> Unit) {
private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_toggle_on),
stringResource(MR.strings.group_preferences),
stringResource(titleId),
click = onClick
)
}
@@ -479,10 +488,10 @@ fun SendReceiptsOptionDisabled() {
}
@Composable
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_add),
stringResource(MR.strings.button_add_members),
stringResource(titleId),
onClick,
iconColor = tint,
textColor = tint
@@ -6,12 +6,10 @@ import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@@ -20,7 +18,6 @@ import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
null to generalGetString(MR.strings.feature_roles_all_members),
@@ -83,7 +80,8 @@ private fun GroupPreferencesLayout(
savePrefs: () -> Unit,
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.group_preferences))
val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
AppBarTitle(stringResource(titleId))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl)))
@@ -136,7 +134,7 @@ private fun GroupPreferencesLayout(
FeatureSection(GroupFeature.History, enableHistory, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
applyPrefs(preferences.copy(history = GroupPreference(enable = enable)))
}
if (groupInfo.canEdit) {
if (groupInfo.isOwner) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ResetSaveButtons(
reset = reset,
@@ -163,12 +161,12 @@ private fun FeatureSection(
val icon = if (on) feature.iconFilled() else feature.icon
val iconTint = if (on) SimplexGreen else MaterialTheme.colors.secondary
val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON
if (groupInfo.canEdit) {
if (groupInfo.isOwner) {
PreferenceToggleWithIcon(
feature.text,
icon,
iconTint,
enableFeature.value == GroupFeatureEnabled.ON,
checked = enableFeature.value == GroupFeatureEnabled.ON,
) { checked ->
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value)
}
@@ -214,7 +212,7 @@ private fun FeatureSection(
onSelected(enableFeature.value, null)
}
}
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit))
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.isOwner))
}
@Composable
@@ -7,9 +7,7 @@ import SectionTextFooter
import SectionView
import TextIconSpaced
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -32,7 +30,6 @@ import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatJsonLength
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@@ -99,7 +96,7 @@ private fun GroupWelcomeLayout(
val editMode = remember { mutableStateOf(true) }
AppBarTitle(stringResource(MR.strings.group_welcome_title))
val wt = rememberSaveable { welcomeText }
if (groupInfo.canEdit) {
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
if (editMode.value) {
val focusRequester = remember { FocusRequester() }
TextEditor(
@@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.unit.*
import chat.simplex.common.model.BusinessChatType
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.model.ChatInfo
@@ -30,7 +31,12 @@ import kotlin.math.max
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false) {
val icon =
when (chatInfo) {
is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled
is ChatInfo.Group ->
when (chatInfo.groupInfo.businessChat?.chatType) {
BusinessChatType.Business -> MR.images.ic_work_filled_padded
BusinessChatType.Customer -> MR.images.ic_account_circle_filled
null -> MR.images.ic_supervised_user_circle_filled
}
is ChatInfo.Local -> MR.images.ic_folder_filled
else -> MR.images.ic_account_circle_filled
}
@@ -275,10 +275,17 @@ suspend fun planAndConnect(
Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo_
if (groupInfo != null) {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + linkText
)
if (groupInfo.businessChat == null) {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + linkText
)
} else {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.connect_plan_chat_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), groupInfo.displayName) + linkText
)
}
} else {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.connect_plan_already_joining_the_group),
@@ -295,11 +302,19 @@ suspend fun planAndConnect(
filterKnownGroup(groupInfo)
} else {
openKnownGroup(chatModel, rhId, close, groupInfo)
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + linkText,
hostDevice = hostDevice(rhId),
)
if (groupInfo.businessChat == null) {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + linkText,
hostDevice = hostDevice(rhId),
)
} else {
AlertManager.privacySensitive.showAlertMsg(
generalGetString(MR.strings.connect_plan_chat_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connected_with_vName), groupInfo.displayName) + linkText,
hostDevice = hostDevice(rhId),
)
}
cleanup?.invoke()
}
}
@@ -123,9 +123,9 @@ private fun TimedMessagesFeatureSection(allowFeature: State<FeatureAllowed>, onS
ChatFeature.TimedMessages.text,
ChatFeature.TimedMessages.icon,
MaterialTheme.colors.secondary,
allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES,
checked = allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES,
extraPadding = false,
onSelected
onChange = onSelected
)
}
SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value))
@@ -408,6 +408,7 @@ fun PreferenceToggleWithIcon(
text: String,
icon: Painter? = null,
iconColor: Color? = MaterialTheme.colors.secondary,
disabled: Boolean = false,
checked: Boolean,
extraPadding: Boolean = false,
onChange: (Boolean) -> Unit = {},
@@ -418,6 +419,7 @@ fun PreferenceToggleWithIcon(
onCheckedChange = {
onChange(it)
},
enabled = !disabled
)
}
}
@@ -196,14 +196,22 @@ private fun UserAddressLayout(
LearnMoreButton(learnMore)
}
} else {
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) }
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
SectionView(stringResource(MR.strings.for_social_media).uppercase()) {
SimpleXLinkQRCode(userAddress.connReqContact)
ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) }
// ShareViaEmailButton { sendEmail(userAddress) }
BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) }
AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas)
if (autoAcceptState.value.business) {
SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations))
}
}
SectionDividerSpaced()
SectionDividerSpaced(maxTopPadding = autoAcceptState.value.business)
SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) {
CreateOneTimeLinkButton()
}
@@ -385,17 +393,37 @@ fun ShareWithContactsButton(shareViaProfile: MutableState<Boolean>, setProfileAd
onDismissRequest = {
shareViaProfile.value = !on
})
}
}
}
@Composable
private fun BusinessAddressToggle(autoAcceptState: MutableState<AutoAcceptState>, saveAas: (AutoAcceptState) -> Unit) {
PreferenceToggleWithIcon(
stringResource(MR.strings.business_address),
painterResource(MR.images.ic_work),
checked = autoAcceptState.value.business,
) { ba ->
autoAcceptState.value = if (ba)
AutoAcceptState(enable = true, incognito = false, business = true, autoAcceptState.value.welcomeText)
else
AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, business = false, autoAcceptState.value.welcomeText)
saveAas(autoAcceptState.value)
}
}
@Composable
private fun AutoAcceptToggle(autoAcceptState: MutableState<AutoAcceptState>, saveAas: (AutoAcceptState) -> Unit) {
PreferenceToggleWithIcon(stringResource(MR.strings.auto_accept_contact), painterResource(MR.images.ic_check), checked = autoAcceptState.value.enable) {
PreferenceToggleWithIcon(
stringResource(MR.strings.auto_accept_contact),
painterResource(MR.images.ic_check),
disabled = autoAcceptState.value.business,
checked = autoAcceptState.value.enable
) {
autoAcceptState.value = if (!it)
AutoAcceptState()
else
AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText)
AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.business, autoAcceptState.value.welcomeText)
saveAas(autoAcceptState.value)
}
}
@@ -416,12 +444,15 @@ private class AutoAcceptState {
private set
var incognito: Boolean = false
private set
var business: Boolean = false
private set
var welcomeText: String = ""
private set
constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") {
constructor(enable: Boolean = false, incognito: Boolean = false, business: Boolean = false, welcomeText: String = "") {
this.enable = enable
this.incognito = incognito
this.business = business
this.welcomeText = welcomeText
}
@@ -429,6 +460,7 @@ private class AutoAcceptState {
contactLink.autoAccept?.let { aa ->
enable = true
incognito = aa.acceptIncognito
business = aa.businessAddress
aa.autoReply?.let { msg ->
welcomeText = msg.text
} ?: run {
@@ -445,19 +477,20 @@ private class AutoAcceptState {
if (s != "") {
autoReply = MsgContent.MCText(s)
}
return AutoAccept(incognito, autoReply)
return AutoAccept(business, incognito, autoReply)
}
return null
}
override fun equals(other: Any?): Boolean {
if (other !is AutoAcceptState) return false
return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText
return this.enable == other.enable && this.incognito == other.incognito && this.business == other.business && this.welcomeText == other.welcomeText
}
override fun hashCode(): Int {
var result = enable.hashCode()
result = 31 * result + incognito.hashCode()
result = 31 * result + business.hashCode()
result = 31 * result + welcomeText.hashCode()
return result
}
@@ -470,7 +503,9 @@ private fun AutoAcceptSection(
saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit
) {
SectionView(stringResource(MR.strings.auto_accept_contact).uppercase()) {
AcceptIncognitoToggle(autoAcceptState)
if (!autoAcceptState.value.business) {
AcceptIncognitoToggle(autoAcceptState)
}
WelcomeMessageEditor(autoAcceptState)
SaveAASButton(autoAcceptState.value == savedAutoAcceptState.value) { saveAas(autoAcceptState.value, savedAutoAcceptState) }
}
@@ -482,9 +517,9 @@ private fun AcceptIncognitoToggle(autoAcceptState: MutableState<AutoAcceptState>
stringResource(MR.strings.accept_contact_incognito_button),
if (autoAcceptState.value.incognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy),
if (autoAcceptState.value.incognito) Indigo else MaterialTheme.colors.secondary,
autoAcceptState.value.incognito,
checked = autoAcceptState.value.incognito,
) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText)
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.business, autoAcceptState.value.welcomeText)
}
}
@@ -494,7 +529,7 @@ private fun WelcomeMessageEditor(autoAcceptState: MutableState<AutoAcceptState>)
TextEditor(welcomeText, Modifier.height(100.dp), placeholder = stringResource(MR.strings.enter_welcome_message_optional))
LaunchedEffect(welcomeText.value) {
if (welcomeText.value != autoAcceptState.value.welcomeText) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value)
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, autoAcceptState.value.business, welcomeText.value)
}
}
}
@@ -936,6 +936,8 @@
<string name="simplex_address_or_1_time_link">SimpleX address or 1-time link?</string>
<string name="create_1_time_link">Create 1-time link</string>
<string name="address_settings">Address settings</string>
<string name="business_address">Business address</string>
<string name="add_your_team_members_to_conversations">Add your team members to the conversations.</string>
<!-- CreateSimpleXAddress.kt -->
<string name="continue_to_next_step">Continue</string>
@@ -1551,6 +1553,8 @@
<!-- GroupChatInfoView.kt -->
<string name="button_add_members">Invite members</string>
<string name="button_add_team_members">Add team members</string>
<string name="button_add_friends">Add friends</string>
<string name="group_info_section_title_num_members">%1$s MEMBERS</string>
<string name="group_info_member_you">you: %1$s</string>
<string name="button_delete_group">Delete group</string>
@@ -2275,10 +2279,12 @@
<string name="connect_plan_open_group">Open group</string>
<string name="connect_plan_repeat_join_request">Repeat join request?</string>
<string name="connect_plan_group_already_exists">Group already exists!</string>
<string name="connect_plan_chat_already_exists">Chat already exists!</string>
<string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[You are already joining the group <b>%1$s</b>.]]></string>
<string name="connect_plan_already_joining_the_group">Already joining the group!</string>
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string>
<string name="connect_plan_you_are_already_in_group_vName"><![CDATA[You are already in group <b>%1$s</b>.]]></string>
<string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[You are already connected with <b>%1$s</b>.]]></string>
<string name="connect_plan_connect_via_link">Connect via link?</string>
<!-- Errors -->
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"><path d="M142.5-125q-22.97 0-40.23-17.27Q85-159.53 85-182.5v-480q0-22.97 17.27-40.23Q119.53-720 142.5-720H323v-97.52q0-22.98 17.27-40.23Q357.53-875 380.5-875h199q22.97 0 40.23 17.27Q637-840.47 637-817.5v97.5h180.5q22.97 0 40.23 17.27Q875-685.47 875-662.5v480q0 22.97-17.27 40.23Q840.47-125 817.5-125h-675Zm0-57.5h675v-480h-675v480Zm238-537.5h199v-97.5h-199v97.5Zm-238 537.5v-480 480Z"/></svg>

After

Width:  |  Height:  |  Size: 495 B

@@ -0,0 +1,8 @@
<svg width="1240" height="1240" xmlns="http://www.w3.org/2000/svg">
<g>
<g>
<path d="m282.5,995q-22.97,0 -40.23,-17.27q-17.27,-17.26 -17.27,-40.23l0,-480q0,-22.97 17.27,-40.23q17.26,-17.27 40.23,-17.27l180.5,0l0,-97.52q0,-22.98 17.27,-40.23q17.26,-17.25 40.23,-17.25l199,0q22.97,0 40.23,17.27q17.27,17.26 17.27,40.23l0,97.5l180.5,0q22.97,0 40.23,17.27q17.27,17.26 17.27,40.23l0,480q0,22.97 -17.27,40.23q-17.26,17.27 -40.23,17.27l-675,0zm238,-595l199,0l0,-97.5l-199,0l0,97.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 504 B