mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 23:00:08 +00:00
Merge branch 'master' into sqlcipher
This commit is contained in:
@@ -47,6 +47,7 @@ class ChatModel(val controller: ChatController) {
|
||||
val runServiceInBackground = mutableStateOf(true)
|
||||
val performLA = mutableStateOf(false)
|
||||
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
|
||||
var incognito = mutableStateOf(false)
|
||||
|
||||
// current WebRTC call
|
||||
val callManager = CallManager(this)
|
||||
@@ -57,7 +58,7 @@ class ChatModel(val controller: ChatController) {
|
||||
val showCallView = mutableStateOf(false)
|
||||
val switchingCall = mutableStateOf(false)
|
||||
|
||||
fun updateUserProfile(profile: Profile) {
|
||||
fun updateUserProfile(profile: LocalProfile) {
|
||||
val user = currentUser.value
|
||||
if (user != null) {
|
||||
currentUser.value = user.copy(profile = profile)
|
||||
@@ -290,19 +291,20 @@ data class User(
|
||||
val userId: Long,
|
||||
val userContactId: Long,
|
||||
val localDisplayName: String,
|
||||
val profile: Profile,
|
||||
val profile: LocalProfile,
|
||||
val activeUser: Boolean
|
||||
): NamedChat {
|
||||
override val displayName: String get() = profile.displayName
|
||||
override val fullName: String get() = profile.fullName
|
||||
override val image: String? get() = profile.image
|
||||
override val localAlias: String = ""
|
||||
|
||||
companion object {
|
||||
val sampleData = User(
|
||||
userId = 1,
|
||||
userContactId = 1,
|
||||
localDisplayName = "alice",
|
||||
profile = Profile.sampleData,
|
||||
profile = LocalProfile.sampleData,
|
||||
activeUser = true
|
||||
)
|
||||
}
|
||||
@@ -314,8 +316,9 @@ interface NamedChat {
|
||||
val displayName: String
|
||||
val fullName: String
|
||||
val image: String?
|
||||
val localAlias: String
|
||||
val chatViewName: String
|
||||
get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName")
|
||||
get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
|
||||
}
|
||||
|
||||
interface SomeChat {
|
||||
@@ -376,6 +379,8 @@ data class Chat (
|
||||
|
||||
@Serializable
|
||||
sealed class ChatInfo: SomeChat, NamedChat {
|
||||
abstract val incognito: Boolean
|
||||
|
||||
@Serializable @SerialName("direct")
|
||||
class Direct(val contact: Contact): ChatInfo() {
|
||||
override val chatType get() = ChatType.Direct
|
||||
@@ -385,11 +390,13 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val ready get() = contact.ready
|
||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contact.chatSettings.enableNtfs
|
||||
override val incognito get() = contact.contactConnIncognito
|
||||
override val createdAt get() = contact.createdAt
|
||||
override val updatedAt get() = contact.updatedAt
|
||||
override val displayName get() = contact.displayName
|
||||
override val fullName get() = contact.fullName
|
||||
override val image get() = contact.image
|
||||
override val localAlias: String get() = contact.localAlias
|
||||
|
||||
companion object {
|
||||
val sampleData = Direct(Contact.sampleData)
|
||||
@@ -405,11 +412,13 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val ready get() = groupInfo.ready
|
||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||
override val ntfsEnabled get() = groupInfo.chatSettings.enableNtfs
|
||||
override val incognito get() = groupInfo.membership.memberIncognito
|
||||
override val createdAt get() = groupInfo.createdAt
|
||||
override val updatedAt get() = groupInfo.updatedAt
|
||||
override val displayName get() = groupInfo.displayName
|
||||
override val fullName get() = groupInfo.fullName
|
||||
override val image get() = groupInfo.image
|
||||
override val localAlias get() = groupInfo.localAlias
|
||||
|
||||
companion object {
|
||||
val sampleData = Group(GroupInfo.sampleData)
|
||||
@@ -425,11 +434,13 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val ready get() = contactRequest.ready
|
||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = false
|
||||
override val createdAt get() = contactRequest.createdAt
|
||||
override val updatedAt get() = contactRequest.updatedAt
|
||||
override val displayName get() = contactRequest.displayName
|
||||
override val fullName get() = contactRequest.fullName
|
||||
override val image get() = contactRequest.image
|
||||
override val localAlias get() = contactRequest.localAlias
|
||||
|
||||
companion object {
|
||||
val sampleData = ContactRequest(UserContactRequest.sampleData)
|
||||
@@ -445,11 +456,13 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val ready get() = contactConnection.ready
|
||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = contactConnection.incognito
|
||||
override val createdAt get() = contactConnection.createdAt
|
||||
override val updatedAt get() = contactConnection.updatedAt
|
||||
override val displayName get() = contactConnection.displayName
|
||||
override val fullName get() = contactConnection.fullName
|
||||
override val image get() = contactConnection.image
|
||||
override val localAlias get() = contactConnection.localAlias
|
||||
|
||||
companion object {
|
||||
fun getSampleData(status: ConnStatus = ConnStatus.New, viaContactUri: Boolean = false): ContactConnection =
|
||||
@@ -462,7 +475,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
data class Contact(
|
||||
val contactId: Long,
|
||||
override val localDisplayName: String,
|
||||
val profile: Profile,
|
||||
val profile: LocalProfile,
|
||||
val activeConn: Connection,
|
||||
val viaGroup: Long? = null,
|
||||
val chatSettings: ChatSettings,
|
||||
@@ -475,18 +488,22 @@ data class Contact(
|
||||
override val ready get() = activeConn.connStatus == ConnStatus.Ready
|
||||
override val sendMsgEnabled get() = true
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||
override val displayName get() = profile.displayName
|
||||
override val displayName get() = localAlias.ifEmpty { profile.displayName }
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
override val localAlias get() = profile.localAlias
|
||||
|
||||
val isIndirectContact: Boolean get() =
|
||||
activeConn.connLevel > 0 || viaGroup != null
|
||||
|
||||
val contactConnIncognito =
|
||||
activeConn.customUserProfileId != null
|
||||
|
||||
companion object {
|
||||
val sampleData = Contact(
|
||||
contactId = 1,
|
||||
localDisplayName = "alice",
|
||||
profile = Profile.sampleData,
|
||||
profile = LocalProfile.sampleData,
|
||||
activeConn = Connection.sampleData,
|
||||
chatSettings = ChatSettings(true),
|
||||
createdAt = Clock.System.now(),
|
||||
@@ -510,10 +527,10 @@ class ContactSubStatus(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int) {
|
||||
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val customUserProfileId: Long? = null) {
|
||||
val id: ChatId get() = ":$connId"
|
||||
companion object {
|
||||
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0)
|
||||
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, customUserProfileId = null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,13 +538,16 @@ class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: In
|
||||
class Profile(
|
||||
override val displayName: String,
|
||||
override val fullName: String,
|
||||
override val image: String? = null
|
||||
override val image: String? = null,
|
||||
override val localAlias : String = ""
|
||||
): NamedChat {
|
||||
val profileViewName: String
|
||||
get() {
|
||||
return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)"
|
||||
}
|
||||
|
||||
fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, image, localAlias)
|
||||
|
||||
companion object {
|
||||
val sampleData = Profile(
|
||||
displayName = "alice",
|
||||
@@ -536,6 +556,28 @@ class Profile(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class LocalProfile(
|
||||
val profileId: Long,
|
||||
override val displayName: String,
|
||||
override val fullName: String,
|
||||
override val image: String? = null,
|
||||
override val localAlias: String,
|
||||
): NamedChat {
|
||||
val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" }
|
||||
|
||||
fun toProfile(): Profile = Profile(displayName, fullName, image, localAlias)
|
||||
|
||||
companion object {
|
||||
val sampleData = LocalProfile(
|
||||
profileId = 1L,
|
||||
displayName = "alice",
|
||||
fullName = "Alice",
|
||||
localAlias = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Group (
|
||||
val groupInfo: GroupInfo,
|
||||
@@ -548,6 +590,7 @@ data class GroupInfo (
|
||||
override val localDisplayName: String,
|
||||
val groupProfile: GroupProfile,
|
||||
val membership: GroupMember,
|
||||
val hostConnCustomUserProfileId: Long? = null,
|
||||
val chatSettings: ChatSettings,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
@@ -561,6 +604,7 @@ data class GroupInfo (
|
||||
override val displayName get() = groupProfile.displayName
|
||||
override val fullName get() = groupProfile.fullName
|
||||
override val image get() = groupProfile.image
|
||||
override val localAlias get() = ""
|
||||
|
||||
val canEdit: Boolean
|
||||
get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent
|
||||
@@ -577,6 +621,7 @@ data class GroupInfo (
|
||||
localDisplayName = "team",
|
||||
groupProfile = GroupProfile.sampleData,
|
||||
membership = GroupMember.sampleData,
|
||||
hostConnCustomUserProfileId = null,
|
||||
chatSettings = ChatSettings(true),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
@@ -588,7 +633,8 @@ data class GroupInfo (
|
||||
class GroupProfile (
|
||||
override val displayName: String,
|
||||
override val fullName: String,
|
||||
override val image: String? = null
|
||||
override val image: String? = null,
|
||||
override val localAlias: String = "",
|
||||
): NamedChat {
|
||||
companion object {
|
||||
val sampleData = GroupProfile(
|
||||
@@ -608,17 +654,18 @@ class GroupMember (
|
||||
var memberStatus: GroupMemberStatus,
|
||||
var invitedBy: InvitedBy,
|
||||
val localDisplayName: String,
|
||||
val memberProfile: Profile,
|
||||
val memberProfile: LocalProfile,
|
||||
val memberContactId: Long? = null,
|
||||
val memberContactProfileId: Long,
|
||||
var activeConn: Connection? = null
|
||||
) {
|
||||
val id: String get() = "#$groupId @$groupMemberId"
|
||||
val displayName: String get() = memberProfile.displayName
|
||||
val displayName: String get() = memberProfile.localAlias.ifEmpty { memberProfile.displayName }
|
||||
val fullName: String get() = memberProfile.fullName
|
||||
val image: String? get() = memberProfile.image
|
||||
|
||||
val chatViewName: String
|
||||
get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName")
|
||||
get() = memberProfile.localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
|
||||
|
||||
val memberActive: Boolean get() = when (this.memberStatus) {
|
||||
GroupMemberStatus.MemRemoved -> false
|
||||
@@ -654,6 +701,8 @@ class GroupMember (
|
||||
&& userRole >= GroupMemberRole.Admin && userRole >= memberRole && membership.memberCurrent
|
||||
}
|
||||
|
||||
val memberIncognito = memberProfile.profileId != memberContactProfileId
|
||||
|
||||
companion object {
|
||||
val sampleData = GroupMember(
|
||||
groupMemberId = 1,
|
||||
@@ -664,8 +713,9 @@ class GroupMember (
|
||||
memberStatus = GroupMemberStatus.MemComplete,
|
||||
invitedBy = InvitedBy.IBUser(),
|
||||
localDisplayName = "alice",
|
||||
memberProfile = Profile.sampleData,
|
||||
memberProfile = LocalProfile.sampleData,
|
||||
memberContactId = 1,
|
||||
memberContactProfileId = 1L,
|
||||
activeConn = Connection.sampleData
|
||||
)
|
||||
}
|
||||
@@ -783,6 +833,7 @@ class UserContactRequest (
|
||||
override val displayName get() = profile.displayName
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
override val localAlias get() = ""
|
||||
|
||||
companion object {
|
||||
val sampleData = UserContactRequest(
|
||||
@@ -801,6 +852,7 @@ class PendingContactConnection(
|
||||
val pccAgentConnId: String,
|
||||
val pccConnStatus: ConnStatus,
|
||||
val viaContactUri: Boolean,
|
||||
val customUserProfileId: Long? = null,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -825,14 +877,21 @@ class PendingContactConnection(
|
||||
}
|
||||
override val fullName get() = ""
|
||||
override val image get() = null
|
||||
override val localAlias get() = ""
|
||||
|
||||
val initiated get() = (pccConnStatus.initiated ?: false) && !viaContactUri
|
||||
|
||||
val incognito = customUserProfileId != null
|
||||
|
||||
val description: String get() {
|
||||
val initiated = pccConnStatus.initiated
|
||||
return if (initiated == null) "" else generalGetString(
|
||||
if (initiated && !viaContactUri) R.string.description_you_shared_one_time_link
|
||||
else if (viaContactUri ) R.string.description_via_contact_address_link
|
||||
else R.string.description_via_one_time_link
|
||||
if (initiated && !viaContactUri)
|
||||
if (incognito) R.string.description_you_shared_one_time_link_incognito else R.string.description_you_shared_one_time_link
|
||||
else if (viaContactUri )
|
||||
if (incognito) R.string.description_via_contact_address_link_incognito else R.string.description_via_contact_address_link
|
||||
else
|
||||
if (incognito) R.string.description_via_one_time_link_incognito else R.string.description_via_one_time_link
|
||||
)
|
||||
}
|
||||
|
||||
@@ -843,6 +902,7 @@ class PendingContactConnection(
|
||||
pccAgentConnId = "abcd",
|
||||
pccConnStatus = status,
|
||||
viaContactUri = viaContactUri,
|
||||
customUserProfileId = null,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
@@ -897,7 +957,7 @@ data class ChatItem (
|
||||
val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew
|
||||
|
||||
val memberDisplayName: String? get() =
|
||||
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.memberProfile.displayName
|
||||
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
|
||||
else null
|
||||
|
||||
val isDeletedContent: Boolean get() =
|
||||
@@ -1190,9 +1250,11 @@ class CIGroupInvitation (
|
||||
val groupMemberId: Long,
|
||||
val localDisplayName: String,
|
||||
val groupProfile: GroupProfile,
|
||||
val status: CIGroupInvitationStatus
|
||||
val status: CIGroupInvitationStatus,
|
||||
) {
|
||||
val text: String get() = String.format(generalGetString(R.string.group_invitation_item_description), groupProfile.displayName)
|
||||
val text: String get() = String.format(
|
||||
generalGetString(R.string.group_invitation_item_description),
|
||||
groupProfile.displayName)
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
|
||||
@@ -95,6 +95,7 @@ class AppPreferences(val context: Context) {
|
||||
val networkTCPKeepIdle = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_IDLE, KeepAliveOpts.defaults.keepIdle)
|
||||
val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults.keepIntvl)
|
||||
val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt)
|
||||
val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false)
|
||||
|
||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
||||
val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb())
|
||||
@@ -165,6 +166,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_IDLE = "NetworkTCPKeepIdle"
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl"
|
||||
private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt"
|
||||
private const val SHARED_PREFS_INCOGNITO = "Incognito"
|
||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
|
||||
}
|
||||
@@ -179,6 +181,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
init {
|
||||
chatModel.runServiceInBackground.value = appPrefs.runServiceInBackground.get()
|
||||
chatModel.performLA.value = appPrefs.performLA.get()
|
||||
chatModel.incognito.value = appPrefs.incognito.get()
|
||||
}
|
||||
|
||||
suspend fun startChat(user: User) {
|
||||
@@ -189,6 +192,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
val justStarted = apiStartChat()
|
||||
if (justStarted) {
|
||||
apiSetFilesFolder(getAppFilesDirectory(appContext))
|
||||
apiSetIncognito(chatModel.incognito.value)
|
||||
chatModel.userAddress.value = apiGetUserAddress()
|
||||
chatModel.userSMPServers.value = getUserSMPServers()
|
||||
val chats = apiGetChats()
|
||||
@@ -297,6 +301,12 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
throw Error("failed to set files folder: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetIncognito(incognito: Boolean) {
|
||||
val r = sendCmd(CC.SetIncognito(incognito))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to set incognito: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiExportArchive(config: ArchiveConfig) {
|
||||
val r = sendCmd(CC.ApiExportArchive(config))
|
||||
if (r is CR.CmdOk) return
|
||||
@@ -405,9 +415,9 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiContactInfo(contactId: Long): ConnectionStats? {
|
||||
suspend fun apiContactInfo(contactId: Long): Pair<ConnectionStats, Profile?>? {
|
||||
val r = sendCmd(CC.APIContactInfo(contactId))
|
||||
if (r is CR.ContactInfo) return r.connectionStats
|
||||
if (r is CR.ContactInfo) return r.connectionStats to r.customUserProfile
|
||||
Log.e(TAG, "apiContactInfo bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
@@ -518,6 +528,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSetContactAlias(contactId: Long, localAlias: String): Contact? {
|
||||
val r = sendCmd(CC.ApiSetContactAlias(contactId, localAlias))
|
||||
if (r is CR.ContactAliasUpdated) return r.toContact
|
||||
Log.e(TAG, "apiSetContactAlias bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateUserAddress(): String? {
|
||||
val r = sendCmd(CC.CreateMyAddress())
|
||||
if (r is CR.UserContactLinkCreated) return r.connReqContact
|
||||
@@ -1133,6 +1150,7 @@ sealed class CC {
|
||||
class StartChat: CC()
|
||||
class ApiStopChat: CC()
|
||||
class SetFilesFolder(val filesFolder: String): CC()
|
||||
class SetIncognito(val incognito: Boolean): CC()
|
||||
class ApiExportArchive(val config: ArchiveConfig): CC()
|
||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||
class ApiDeleteStorage: CC()
|
||||
@@ -1163,6 +1181,7 @@ sealed class CC {
|
||||
class ListContacts: CC()
|
||||
class ApiUpdateProfile(val profile: Profile): CC()
|
||||
class ApiParseMarkdown(val text: String): CC()
|
||||
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
||||
class CreateMyAddress: CC()
|
||||
class DeleteMyAddress: CC()
|
||||
class ShowMyAddress: CC()
|
||||
@@ -1185,6 +1204,7 @@ sealed class CC {
|
||||
is StartChat -> "/_start"
|
||||
is ApiStopChat -> "/_stop"
|
||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||
is SetIncognito -> "/incognito ${if (incognito) "on" else "off"}"
|
||||
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||
is ApiDeleteStorage -> "/_db delete"
|
||||
@@ -1214,6 +1234,7 @@ sealed class CC {
|
||||
is ListContacts -> "/contacts"
|
||||
is ApiUpdateProfile -> "/_profile ${json.encodeToString(profile)}"
|
||||
is ApiParseMarkdown -> "/_parse $text"
|
||||
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
||||
is CreateMyAddress -> "/address"
|
||||
is DeleteMyAddress -> "/delete_address"
|
||||
is ShowMyAddress -> "/show_address"
|
||||
@@ -1237,6 +1258,7 @@ sealed class CC {
|
||||
is StartChat -> "startChat"
|
||||
is ApiStopChat -> "apiStopChat"
|
||||
is SetFilesFolder -> "setFilesFolder"
|
||||
is SetIncognito -> "setIncognito"
|
||||
is ApiExportArchive -> "apiExportArchive"
|
||||
is ApiImportArchive -> "apiImportArchive"
|
||||
is ApiDeleteStorage -> "apiDeleteStorage"
|
||||
@@ -1266,6 +1288,7 @@ sealed class CC {
|
||||
is ListContacts -> "listContacts"
|
||||
is ApiUpdateProfile -> "updateProfile"
|
||||
is ApiParseMarkdown -> "apiParseMarkdown"
|
||||
is ApiSetContactAlias -> "apiSetContactAlias"
|
||||
is CreateMyAddress -> "createMyAddress"
|
||||
is DeleteMyAddress -> "deleteMyAddress"
|
||||
is ShowMyAddress -> "showMyAddress"
|
||||
@@ -1412,7 +1435,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR()
|
||||
@Serializable @SerialName("userSMPServers") class UserSMPServers(val smpServers: List<String>): CR()
|
||||
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
|
||||
@Serializable @SerialName("contactInfo") class ContactInfo(val contact: Contact, val connectionStats: ConnectionStats): CR()
|
||||
@Serializable @SerialName("contactInfo") class ContactInfo(val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR()
|
||||
@Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats?): CR()
|
||||
@Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR()
|
||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation: CR()
|
||||
@@ -1422,6 +1445,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("chatCleared") class ChatCleared(val chatInfo: ChatInfo): CR()
|
||||
@Serializable @SerialName("userProfileNoChange") class UserProfileNoChange: CR()
|
||||
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR()
|
||||
@Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR()
|
||||
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
|
||||
@Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR()
|
||||
@Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR()
|
||||
@@ -1508,6 +1532,7 @@ sealed class CR {
|
||||
is ChatCleared -> "chatCleared"
|
||||
is UserProfileNoChange -> "userProfileNoChange"
|
||||
is UserProfileUpdated -> "userProfileUpdated"
|
||||
is ContactAliasUpdated -> "contactAliasUpdated"
|
||||
is ParsedMarkdown -> "apiParsedMarkdown"
|
||||
is UserContactLink -> "userContactLink"
|
||||
is UserContactLinkCreated -> "userContactLinkCreated"
|
||||
@@ -1592,6 +1617,7 @@ sealed class CR {
|
||||
is ChatCleared -> json.encodeToString(chatInfo)
|
||||
is UserProfileNoChange -> noDetails()
|
||||
is UserProfileUpdated -> json.encodeToString(toProfile)
|
||||
is ContactAliasUpdated -> json.encodeToString(toContact)
|
||||
is ParsedMarkdown -> json.encodeToString(formattedText)
|
||||
is UserContactLink -> connReqContact
|
||||
is UserContactLinkCreated -> connReqContact
|
||||
|
||||
@@ -7,6 +7,7 @@ val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
||||
val Gray = Color(0x22222222)
|
||||
val Indigo = Color(0xff330099)
|
||||
val SimplexBlue = Color(0, 136, 255, 255) // If this value changes also need to update #0088ff in string resource files
|
||||
val SimplexGreen = Color(77, 218, 103, 255)
|
||||
val SecretColor = Color(0x40808080)
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
@@ -22,7 +23,9 @@ import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -33,15 +36,29 @@ import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun ChatInfoView(chatModel: ChatModel, connStats: ConnectionStats?, close: () -> Unit) {
|
||||
fun ChatInfoView(
|
||||
chatModel: ChatModel,
|
||||
contact: Contact,
|
||||
connStats: ConnectionStats?,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
close: () -> Unit,
|
||||
onChatUpdated: (Chat) -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null) {
|
||||
ChatInfoLayout(
|
||||
chat,
|
||||
contact,
|
||||
connStats,
|
||||
customUserProfile,
|
||||
localAlias,
|
||||
developerTools,
|
||||
onLocalAliasChanged = {
|
||||
setContactAlias(chat.chatInfo.apiId, it, chatModel, onChatUpdated)
|
||||
},
|
||||
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
changeNtfsState = { enabled ->
|
||||
@@ -120,8 +137,12 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit
|
||||
@Composable
|
||||
fun ChatInfoLayout(
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
connStats: ConnectionStats?,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
developerTools: Boolean,
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
deleteContact: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
changeNtfsState: (Boolean) -> Unit,
|
||||
@@ -136,8 +157,18 @@ fun ChatInfoLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
ChatInfoHeader(chat.chatInfo)
|
||||
ChatInfoHeader(chat.chatInfo, contact)
|
||||
}
|
||||
|
||||
LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged)
|
||||
|
||||
if (customUserProfile != null) {
|
||||
SectionSpacer()
|
||||
SectionView(generalGetString(R.string.incognito).uppercase()) {
|
||||
InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName)
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (connStats != null) {
|
||||
@@ -193,19 +224,20 @@ fun ChatInfoLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInfoHeader(cInfo: ChatInfo) {
|
||||
fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Text(
|
||||
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
|
||||
Text(
|
||||
cInfo.fullName, style = MaterialTheme.typography.h2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
@@ -216,6 +248,31 @@ fun ChatInfoHeader(cInfo: ChatInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) {
|
||||
var value by remember { mutableStateOf(initialValue) }
|
||||
DefaultBasicTextField(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 10.dp),
|
||||
initialValue,
|
||||
{
|
||||
Text(
|
||||
generalGetString(R.string.text_field_set_contact_placeholder),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
},
|
||||
color = HighOrLowlight,
|
||||
textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center),
|
||||
keyboardActions = KeyboardActions(onDone = { updateValue(value) })
|
||||
) {
|
||||
value = it
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { updateValue(value) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
|
||||
Row(
|
||||
@@ -348,6 +405,13 @@ fun DeleteContactButton(deleteContact: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel, onChatUpdated: (Chat) -> Unit) = withApi {
|
||||
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
|
||||
chatModel.updateContact(it)
|
||||
onChatUpdated(chatModel.getChat(chatModel.chatId.value ?: return@withApi) ?: return@withApi)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
@@ -358,9 +422,13 @@ fun PreviewChatInfoLayout() {
|
||||
chatItems = arrayListOf(),
|
||||
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
|
||||
),
|
||||
Contact.sampleData,
|
||||
localAlias = "",
|
||||
changeNtfsState = {},
|
||||
developerTools = false,
|
||||
connStats = null,
|
||||
onLocalAliasChanged = {},
|
||||
customUserProfile = null,
|
||||
deleteContact = {}, clearChat = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,18 +105,21 @@ fun ChatView(chatModel: ChatModel) {
|
||||
chatModel.chatItems,
|
||||
searchText,
|
||||
useLinkPreviews = useLinkPreviews,
|
||||
chatModelIncognito = chatModel.incognito.value,
|
||||
back = { chatModel.chatId.value = null },
|
||||
info = {
|
||||
withApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val connStats = chatModel.controller.apiContactInfo(cInfo.apiId)
|
||||
val contactInfo = chatModel.controller.apiContactInfo(cInfo.apiId)
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
ChatInfoView(chatModel, connStats, close)
|
||||
ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close) {
|
||||
activeChat = it
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cInfo is ChatInfo.Group) {
|
||||
@@ -191,7 +194,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
AddGroupMembersView(groupInfo, chatModel, close)
|
||||
AddGroupMembersView(groupInfo, chatModel, true, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,6 +235,7 @@ fun ChatLayout(
|
||||
chatItems: List<ChatItem>,
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
chatModelIncognito: Boolean,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
@@ -277,7 +281,7 @@ fun ChatLayout(
|
||||
BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) {
|
||||
ChatItemsList(
|
||||
user, chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage,
|
||||
useLinkPreviews, chatModelIncognito, openDirectChat, loadPrevMessages, deleteMessage,
|
||||
receiveFile, joinGroup, acceptCall, markRead, setFloatingButton
|
||||
)
|
||||
}
|
||||
@@ -331,7 +335,7 @@ fun ChatInfoToolbar(
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) {
|
||||
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu = false
|
||||
@@ -375,6 +379,9 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (cInfo.incognito) {
|
||||
IncognitoImage(size = 36.dp, Indigo)
|
||||
}
|
||||
ChatInfoImage(cInfo, size = imageSize, iconColor)
|
||||
Column(
|
||||
Modifier.padding(start = 8.dp),
|
||||
@@ -384,7 +391,7 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
|
||||
cInfo.displayName, fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.localAlias.isEmpty()) {
|
||||
Text(
|
||||
cInfo.fullName,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
@@ -415,6 +422,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
chatItems: List<ChatItem>,
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
chatModelIncognito: Boolean,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
@@ -514,11 +522,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
@@ -529,7 +537,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
end = if (sent) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall)
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,6 +823,7 @@ fun PreviewChatLayout() {
|
||||
chatItems = chatItems,
|
||||
searchValue,
|
||||
useLinkPreviews = true,
|
||||
chatModelIncognito = false,
|
||||
back = {},
|
||||
info = {},
|
||||
openDirectChat = {},
|
||||
@@ -871,6 +880,7 @@ fun PreviewGroupChatLayout() {
|
||||
chatItems = chatItems,
|
||||
searchValue,
|
||||
useLinkPreviews = true,
|
||||
chatModelIncognito = false,
|
||||
back = {},
|
||||
info = {},
|
||||
openDirectChat = {},
|
||||
|
||||
+50
-9
@@ -11,11 +11,14 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -28,7 +31,7 @@ import chat.simplex.app.views.chat.ChatInfoToolbarTitle
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
|
||||
fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, showFooterCounter: Boolean = true, close: () -> Unit) {
|
||||
val selectedContacts = remember { mutableStateListOf<Long>() }
|
||||
val selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) }
|
||||
|
||||
@@ -38,6 +41,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () ->
|
||||
contactsToAdd = getContactsToAdd(chatModel),
|
||||
selectedContacts = selectedContacts,
|
||||
selectedRole = selectedRole,
|
||||
showFooterCounter = showFooterCounter,
|
||||
inviteMembers = {
|
||||
withApi {
|
||||
selectedContacts.forEach {
|
||||
@@ -75,6 +79,7 @@ fun AddGroupMembersLayout(
|
||||
contactsToAdd: List<Contact>,
|
||||
selectedContacts: SnapshotStateList<Long>,
|
||||
selectedRole: MutableState<GroupMemberRole>,
|
||||
showFooterCounter: Boolean,
|
||||
inviteMembers: () -> Unit,
|
||||
clearSelection: () -> Unit,
|
||||
addContact: (Long) -> Unit,
|
||||
@@ -119,13 +124,15 @@ fun AddGroupMembersLayout(
|
||||
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty())
|
||||
}
|
||||
}
|
||||
SectionCustomFooter {
|
||||
InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection)
|
||||
if (showFooterCounter) {
|
||||
SectionCustomFooter {
|
||||
InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, addContact, removeContact)
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, addContact, removeContact)
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
@@ -255,6 +262,7 @@ fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit)
|
||||
fun ContactList(
|
||||
contacts: List<Contact>,
|
||||
selectedContacts: SnapshotStateList<Long>,
|
||||
groupInfo: GroupInfo,
|
||||
addContact: (Long) -> Unit,
|
||||
removeContact: (Long) -> Unit
|
||||
) {
|
||||
@@ -262,7 +270,7 @@ fun ContactList(
|
||||
contacts.forEachIndexed { index, contact ->
|
||||
SectionItemView {
|
||||
ContactCheckRow(
|
||||
contact, addContact, removeContact,
|
||||
contact, groupInfo, addContact, removeContact,
|
||||
checked = selectedContacts.contains(contact.apiId)
|
||||
)
|
||||
}
|
||||
@@ -276,14 +284,35 @@ fun ContactList(
|
||||
@Composable
|
||||
fun ContactCheckRow(
|
||||
contact: Contact,
|
||||
groupInfo: GroupInfo,
|
||||
addContact: (Long) -> Unit,
|
||||
removeContact: (Long) -> Unit,
|
||||
checked: Boolean
|
||||
) {
|
||||
val prohibitedToInviteIncognito = !groupInfo.membership.memberIncognito && contact.contactConnIncognito
|
||||
val icon: ImageVector
|
||||
val iconColor: Color
|
||||
if (prohibitedToInviteIncognito) {
|
||||
icon = Icons.Filled.TheaterComedy
|
||||
iconColor = HighOrLowlight
|
||||
} else if (checked) {
|
||||
icon = Icons.Filled.CheckCircle
|
||||
iconColor = MaterialTheme.colors.primary
|
||||
} else {
|
||||
icon = Icons.Outlined.Circle
|
||||
iconColor = HighOrLowlight
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clickable { if (!checked) addContact(contact.apiId) else removeContact(contact.apiId) },
|
||||
.clickable {
|
||||
if (prohibitedToInviteIncognito) {
|
||||
showProhibitedToInviteIncognitoAlertDialog()
|
||||
} else if (!checked)
|
||||
addContact(contact.apiId)
|
||||
else
|
||||
removeContact(contact.apiId)
|
||||
},
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -292,16 +321,27 @@ fun ContactCheckRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
ProfileImage(size = 36.dp, contact.image)
|
||||
Text(contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(
|
||||
contact.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = if (prohibitedToInviteIncognito) HighOrLowlight else Color.Unspecified
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
if (checked) Icons.Filled.CheckCircle else Icons.Outlined.Circle,
|
||||
icon,
|
||||
contentDescription = stringResource(R.string.icon_descr_contact_checked),
|
||||
tint = if (checked) MaterialTheme.colors.primary else HighOrLowlight
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showProhibitedToInviteIncognitoAlertDialog() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.invite_prohibited),
|
||||
text = generalGetString(R.string.invite_prohibited_description),
|
||||
confirmText = generalGetString(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewAddGroupMembersLayout() {
|
||||
@@ -311,6 +351,7 @@ fun PreviewAddGroupMembersLayout() {
|
||||
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
|
||||
selectedContacts = remember { mutableStateListOf() },
|
||||
selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) },
|
||||
showFooterCounter = true,
|
||||
inviteMembers = {},
|
||||
clearSelection = {},
|
||||
addContact = {},
|
||||
|
||||
+38
-9
@@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -24,6 +25,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
import chat.simplex.app.views.chatlist.cantInviteIncognitoAlert
|
||||
import chat.simplex.app.views.chatlist.setGroupMembers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@@ -49,20 +51,20 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
AddGroupMembersView(groupInfo, chatModel, close)
|
||||
AddGroupMembersView(groupInfo, chatModel, true, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
showMemberInfo = { member ->
|
||||
withApi {
|
||||
val connStats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
GroupMemberInfoView(groupInfo, member, connStats, chatModel, close)
|
||||
GroupMemberInfoView(groupInfo, member, stats, chatModel, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,14 +139,16 @@ fun GroupChatInfoLayout(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
ChatInfoHeader(chat.chatInfo)
|
||||
GroupChatInfoHeader(chat.chatInfo)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
SectionItemView {
|
||||
AddMembersButton(addMembers)
|
||||
val tint = if (chat.chatInfo.incognito) HighOrLowlight else MaterialTheme.colors.primary
|
||||
val onClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
|
||||
AddMembersButton(tint, onClick)
|
||||
}
|
||||
SectionDivider()
|
||||
}
|
||||
@@ -206,7 +210,31 @@ fun GroupChatInfoLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddMembersButton(addMembers: () -> Unit) {
|
||||
fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Text(
|
||||
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
||||
Text(
|
||||
cInfo.fullName, style = MaterialTheme.typography.h2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, addMembers: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -216,10 +244,10 @@ fun AddMembersButton(addMembers: () -> Unit) {
|
||||
Icon(
|
||||
Icons.Outlined.Add,
|
||||
stringResource(R.string.button_add_members),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = tint
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.button_add_members), color = MaterialTheme.colors.primary)
|
||||
Text(stringResource(R.string.button_add_members), color = tint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +279,8 @@ fun MemberRow(member: GroupMember, showMemberInfo: ((GroupMember) -> Unit)? = nu
|
||||
) {
|
||||
ProfileImage(size = 46.dp, member.image)
|
||||
Column {
|
||||
Text(member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = if (member.memberIncognito) Indigo else Color.Unspecified)
|
||||
val s = member.memberStatus.shortText
|
||||
val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s
|
||||
Text(
|
||||
|
||||
+7
-1
@@ -27,7 +27,13 @@ import chat.simplex.app.views.chat.SimplexServers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoView(groupInfo: GroupInfo, member: GroupMember, connStats: ConnectionStats?, chatModel: ChatModel, close: () -> Unit) {
|
||||
fun GroupMemberInfoView(
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
connStats: ConnectionStats?,
|
||||
chatModel: ChatModel,
|
||||
close: () -> Unit
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
|
||||
+9
-10
@@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
@@ -28,6 +27,7 @@ fun CIGroupInvitationView(
|
||||
ci: ChatItem,
|
||||
groupInvitation: CIGroupInvitation,
|
||||
memberRole: GroupMemberRole,
|
||||
chatIncognito: Boolean = false,
|
||||
joinGroup: (Long) -> Unit
|
||||
) {
|
||||
val sent = ci.chatDir.sent
|
||||
@@ -37,7 +37,7 @@ fun CIGroupInvitationView(
|
||||
fun groupInfoView() {
|
||||
val p = groupInvitation.groupProfile
|
||||
val iconColor =
|
||||
if (action) MaterialTheme.colors.primary
|
||||
if (action) if (chatIncognito) Indigo else MaterialTheme.colors.primary
|
||||
else if (isInDarkTheme()) FileDark else FileLight
|
||||
|
||||
Row(
|
||||
@@ -46,7 +46,7 @@ fun CIGroupInvitationView(
|
||||
.padding(vertical = 4.dp)
|
||||
.padding(end = 2.dp)
|
||||
) {
|
||||
ProfileImage(size = 60.dp, icon = Icons.Filled.SupervisedUserCircle, color = iconColor)
|
||||
ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = Icons.Filled.SupervisedUserCircle, color = iconColor)
|
||||
Spacer(Modifier.padding(horizontal = 3.dp))
|
||||
Column(
|
||||
Modifier.defaultMinSize(minHeight = 60.dp),
|
||||
@@ -71,13 +71,10 @@ fun CIGroupInvitationView(
|
||||
}
|
||||
}
|
||||
|
||||
fun acceptInvitation() {
|
||||
Log.d(TAG, "CIGroupInvitationView acceptInvitation")
|
||||
joinGroup(groupInvitation.groupId)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = if (action) Modifier.clickable(onClick = ::acceptInvitation) else Modifier,
|
||||
modifier = if (action) Modifier.clickable(onClick = {
|
||||
joinGroup(groupInvitation.groupId)
|
||||
}) else Modifier,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (sent) SentColorLight else ReceivedColorLight,
|
||||
) {
|
||||
@@ -99,7 +96,9 @@ fun CIGroupInvitationView(
|
||||
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
|
||||
if (action) {
|
||||
groupInvitationText()
|
||||
Text(stringResource(R.string.group_invitation_tap_to_join), color = MaterialTheme.colors.primary)
|
||||
Text(stringResource(
|
||||
if (chatIncognito) R.string.group_invitation_tap_to_join_incognito else R.string.group_invitation_tap_to_join),
|
||||
color = if (chatIncognito) Indigo else MaterialTheme.colors.primary)
|
||||
} else {
|
||||
Box(Modifier.padding(end = 48.dp)) {
|
||||
groupInvitationText()
|
||||
|
||||
@@ -35,6 +35,7 @@ fun ChatItemView(
|
||||
cxt: Context,
|
||||
uriHandler: UriHandler? = null,
|
||||
showMember: Boolean = false,
|
||||
chatModelIncognito: Boolean,
|
||||
useLinkPreviews: Boolean,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -147,8 +148,8 @@ fun ChatItemView(
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIGroupEventView(cItem)
|
||||
is CIContent.SndGroupEventContent -> CIGroupEventView(cItem)
|
||||
}
|
||||
@@ -213,6 +214,7 @@ fun PreviewChatItemView() {
|
||||
useLinkPreviews = true,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
cxt = LocalContext.current,
|
||||
chatModelIncognito = false,
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
joinGroup = {},
|
||||
@@ -232,6 +234,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
useLinkPreviews = true,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
cxt = LocalContext.current,
|
||||
chatModelIncognito = false,
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
joinGroup = {},
|
||||
|
||||
@@ -31,12 +31,19 @@ fun EmojiText(text: String) {
|
||||
Text(s, style = if (s.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont)
|
||||
}
|
||||
|
||||
private fun isSimpleEmoji(c: Int): Boolean = c > 0x238C
|
||||
// https://stackoverflow.com/a/46279500
|
||||
private const val emojiStr = "^(" +
|
||||
"(?:[\\u2700-\\u27bf]|" +
|
||||
"(?:[\\ud83c\\udde6-\\ud83c\\uddff]){2}|" +
|
||||
"[\\ud800\\udc00-\\uDBFF\\uDFFF]|[\\u2600-\\u26FF])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe23\\u20d0-\\u20f0]|[\\ud83c\\udffb-\\ud83c\\udfff])?" +
|
||||
"(?:\\u200d(?:[^\\ud800-\\udfff]|" +
|
||||
"(?:[\\ud83c\\udde6-\\ud83c\\uddff]){2}|" +
|
||||
"[\\ud800\\udc00-\\uDBFF\\uDFFF]|[\\u2600-\\u26FF])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe23\\u20d0-\\u20f0]|[\\ud83c\\udffb-\\ud83c\\udfff])?)*|" +
|
||||
"[\\u0023-\\u0039]\\ufe0f?\\u20e3|\\u3299|\\u3297|\\u303d|\\u3030|\\u24c2|[\\ud83c\\udd70-\\ud83c\\udd71]|[\\ud83c\\udd7e-\\ud83c\\udd7f]|\\ud83c\\udd8e|[\\ud83c\\udd91-\\ud83c\\udd9a]|[\\ud83c\\udde6-\\ud83c\\uddff]|[\\ud83c\\ude01-\\ud83c\\ude02]|\\ud83c\\ude1a|\\ud83c\\ude2f|[\\ud83c\\ude32-\\ud83c\\ude3a]|[\\ud83c\\ude50-\\ud83c\\ude51]|\\u203c|\\u2049|[\\u25aa-\\u25ab]|\\u25b6|\\u25c0|[\\u25fb-\\u25fe]|\\u00a9|\\u00ae|\\u2122|\\u2139|\\ud83c\\udc04|[\\u2600-\\u26FF]|\\u2b05|\\u2b06|\\u2b07|\\u2b1b|\\u2b1c|\\u2b50|\\u2b55|\\u231a|\\u231b|\\u2328|\\u23cf|[\\u23e9-\\u23f3]|[\\u23f8-\\u23fa]|\\ud83c\\udccf|\\u2934|\\u2935|[\\u2190-\\u21ff]" +
|
||||
")+$" // Multiple matches with emojis where one follows another without interruptions from other characters
|
||||
private val emojiRegex = Regex(emojiStr)
|
||||
|
||||
fun isEmoji(c: Int): Boolean = isSimpleEmoji(c) // || isCombinedIntoEmoji(c)
|
||||
|
||||
// TODO count perceived emojis, possibly using icu4j
|
||||
fun isShortEmoji(str: String): Boolean {
|
||||
val s = str.trim()
|
||||
return s.codePoints().count() in 1..5 && s.codePoints().allMatch(::isEmoji)
|
||||
return s.codePoints().count() in 1..5 && emojiRegex.matches(str)
|
||||
}
|
||||
|
||||
+30
-15
@@ -5,6 +5,7 @@ import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -15,8 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.WarningOrange
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
import chat.simplex.app.views.chat.group.deleteGroupDialog
|
||||
import chat.simplex.app.views.chat.group.leaveGroupDialog
|
||||
@@ -38,7 +38,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, stopped) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped) },
|
||||
click = { directChatAction(chat.chatInfo, chatModel) },
|
||||
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
@@ -46,7 +46,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
)
|
||||
is ChatInfo.Group ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, stopped) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped) },
|
||||
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
||||
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
@@ -54,7 +54,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
)
|
||||
is ChatInfo.ContactRequest ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ContactRequestView(chat.chatInfo) },
|
||||
chatLinkPreview = { ContactRequestView(chatModel.incognito.value, chat.chatInfo) },
|
||||
click = { contactRequestAlertDialog(chat.chatInfo, chatModel) },
|
||||
dropdownMenuItems = { ContactRequestMenuItems(chat.chatInfo, chatModel, showMenu) },
|
||||
showMenu,
|
||||
@@ -128,7 +128,7 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Bo
|
||||
fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
|
||||
when (groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> {
|
||||
JoinGroupAction(groupInfo, chatModel, showMenu)
|
||||
JoinGroupAction(chat, groupInfo, chatModel, showMenu)
|
||||
if (groupInfo.canDelete) {
|
||||
DeleteGroupAction(chat, chatModel, showMenu)
|
||||
}
|
||||
@@ -214,12 +214,14 @@ fun DeleteGroupAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<B
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun JoinGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
val joinGroup: () -> Unit = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } }
|
||||
ItemAction(
|
||||
stringResource(R.string.join_group_button),
|
||||
Icons.Outlined.Login,
|
||||
if (chat.chatInfo.incognito) stringResource(R.string.join_group_incognito_button) else stringResource(R.string.join_group_button),
|
||||
if (chat.chatInfo.incognito) Icons.Filled.TheaterComedy else Icons.Outlined.Login,
|
||||
color = if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) }
|
||||
joinGroup()
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -241,8 +243,9 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab
|
||||
@Composable
|
||||
fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(R.string.accept_contact_button),
|
||||
Icons.Outlined.Check,
|
||||
if (chatModel.incognito.value) stringResource(R.string.accept_contact_incognito_button) else stringResource(R.string.accept_contact_button),
|
||||
if (chatModel.incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.Check,
|
||||
color = if (chatModel.incognito.value) Indigo else MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(chatInfo, chatModel)
|
||||
showMenu.value = false
|
||||
@@ -291,7 +294,7 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.accept_connection_request__question),
|
||||
text = generalGetString(R.string.if_you_choose_to_reject_the_sender_will_not_be_notified),
|
||||
confirmText = generalGetString(R.string.accept_contact_button),
|
||||
confirmText = if (chatModel.incognito.value) generalGetString(R.string.accept_contact_incognito_button) else generalGetString(R.string.accept_contact_button),
|
||||
onConfirm = { acceptContactRequest(contactRequest, chatModel) },
|
||||
dismissText = generalGetString(R.string.reject_contact_button),
|
||||
onDismiss = { rejectContactRequest(contactRequest, chatModel) }
|
||||
@@ -388,13 +391,21 @@ fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.join_group_question),
|
||||
text = generalGetString(R.string.you_are_invited_to_group_join_to_connect_with_group_members),
|
||||
confirmText = generalGetString(R.string.join_group_button),
|
||||
confirmText = if (groupInfo.membership.memberIncognito) generalGetString(R.string.join_group_incognito_button) else generalGetString(R.string.join_group_button),
|
||||
onConfirm = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } },
|
||||
dismissText = generalGetString(R.string.delete_verb),
|
||||
onDismiss = { deleteGroup(groupInfo, chatModel) }
|
||||
)
|
||||
}
|
||||
|
||||
fun cantInviteIncognitoAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.alert_title_cant_invite_contacts),
|
||||
text = generalGetString(R.string.alert_title_cant_invite_contacts_descr),
|
||||
confirmText = generalGetString(R.string.ok),
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteChat(ChatType.Group, groupInfo.apiId)
|
||||
@@ -473,6 +484,8 @@ fun PreviewChatListNavLinkDirect() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
false,
|
||||
null,
|
||||
stopped = false
|
||||
)
|
||||
},
|
||||
@@ -508,6 +521,8 @@ fun PreviewChatListNavLinkGroup() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
false,
|
||||
null,
|
||||
stopped = false
|
||||
)
|
||||
},
|
||||
@@ -530,7 +545,7 @@ fun PreviewChatListNavLinkContactRequest() {
|
||||
SimpleXTheme {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
ContactRequestView(ChatInfo.ContactRequest.sampleData)
|
||||
ContactRequestView(false, ChatInfo.ContactRequest.sampleData)
|
||||
},
|
||||
click = {},
|
||||
dropdownMenuItems = null,
|
||||
|
||||
@@ -8,8 +8,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -17,6 +19,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.Indigo
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.NewChatSheet
|
||||
import chat.simplex.app.views.onboarding.MakeConnection
|
||||
@@ -68,7 +71,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse()
|
||||
}
|
||||
BottomSheetScaffold(
|
||||
topBar = { ChatListToolbar(scaffoldCtrl, stopped) },
|
||||
topBar = { ChatListToolbar(chatModel, scaffoldCtrl, stopped) },
|
||||
scaffoldState = scaffoldCtrl.state,
|
||||
drawerContent = { SettingsView(chatModel, setPerformLA) },
|
||||
sheetPeekHeight = 0.dp,
|
||||
@@ -100,15 +103,25 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatListToolbar(scaffoldCtrl: ScaffoldController, stopped: Boolean) {
|
||||
fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stopped: Boolean) {
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { NavigationButtonMenu { scaffoldCtrl.toggleDrawer() } },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.your_chats),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
stringResource(R.string.your_chats),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (chatModel.incognito.value) {
|
||||
Icon(
|
||||
Icons.Filled.TheaterComedy,
|
||||
stringResource(R.string.incognito),
|
||||
tint = Indigo,
|
||||
modifier = Modifier.padding(10.dp).size(26.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onTitleClick = null,
|
||||
showSearch = false,
|
||||
|
||||
@@ -26,7 +26,7 @@ import chat.simplex.app.views.chat.item.MarkdownText
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean) {
|
||||
val cInfo = chat.chatInfo
|
||||
|
||||
@Composable
|
||||
@@ -70,7 +70,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight)
|
||||
is ChatInfo.Group ->
|
||||
when (cInfo.groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> chatPreviewTitleText(MaterialTheme.colors.primary)
|
||||
GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary)
|
||||
GroupMemberStatus.MemAccepted -> chatPreviewTitleText(HighOrLowlight)
|
||||
else -> chatPreviewTitleText()
|
||||
}
|
||||
@@ -79,7 +79,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun chatPreviewText() {
|
||||
fun chatPreviewText(chatModelIncognito: Boolean) {
|
||||
val ci = chat.chatItems.lastOrNull()
|
||||
if (ci != null) {
|
||||
MarkdownText(
|
||||
@@ -97,7 +97,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
when (cInfo.groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> Text(stringResource(R.string.group_preview_you_are_invited))
|
||||
GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(chatModelIncognito, currentUserProfileDisplayName, cInfo.groupInfo))
|
||||
GroupMemberStatus.MemAccepted -> Text(stringResource(R.string.group_connection_pending), color = HighOrLowlight)
|
||||
else -> {}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
.weight(1F)
|
||||
) {
|
||||
chatPreviewTitle()
|
||||
chatPreviewText()
|
||||
chatPreviewText(chatModelIncognito)
|
||||
}
|
||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
|
||||
|
||||
@@ -178,6 +178,16 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun groupInvitationPreviewText(chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String {
|
||||
return if (groupInfo.membership.memberIncognito)
|
||||
String.format(stringResource(R.string.group_preview_join_as), groupInfo.membership.memberProfile.displayName)
|
||||
else if (chatModelIncognito)
|
||||
String.format(stringResource(R.string.group_preview_join_as), currentUserProfileDisplayName ?: "")
|
||||
else
|
||||
stringResource(R.string.group_preview_you_are_invited)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun unreadCountStr(n: Int): String {
|
||||
return if (n < 1000) "$n" else "${n / 1000}" + stringResource(R.string.thousand_abbreviation)
|
||||
@@ -215,6 +225,6 @@ fun ChatStatusImage(chat: Chat) {
|
||||
@Composable
|
||||
fun PreviewChatPreviewView() {
|
||||
SimpleXTheme {
|
||||
ChatPreviewView(Chat.sampleData, stopped = false)
|
||||
ChatPreviewView(Chat.sampleData, false, "", stopped = false)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -10,13 +10,12 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatInfo
|
||||
import chat.simplex.app.model.getTimestampText
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ChatInfoImage
|
||||
|
||||
@Composable
|
||||
fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) {
|
||||
fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.ContactRequest) {
|
||||
Row {
|
||||
ChatInfoImage(contactRequest, size = 72.dp)
|
||||
Column(
|
||||
@@ -30,7 +29,7 @@ fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) {
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h3,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.primary
|
||||
color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary
|
||||
)
|
||||
Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
|
||||
@@ -45,7 +46,8 @@ class AlertManager {
|
||||
confirmText: String = generalGetString(R.string.ok),
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
dismissText: String = generalGetString(R.string.cancel_verb),
|
||||
onDismiss: (() -> Unit)? = null
|
||||
onDismiss: (() -> Unit)? = null,
|
||||
destructive: Boolean = false
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
@@ -57,7 +59,7 @@ class AlertManager {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText) }
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
|
||||
@@ -6,8 +6,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.SupervisedUserCircle
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -31,6 +30,17 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme
|
||||
ProfileImage(size, chatInfo.image, icon, iconColor)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondary) {
|
||||
Box(Modifier.size(size)) {
|
||||
Icon(
|
||||
Icons.Filled.TheaterComedy, stringResource(R.string.incognito),
|
||||
modifier = Modifier.size(size).padding(size / 12),
|
||||
iconColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileImage(
|
||||
size: Dp,
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DefaultBasicTextField(
|
||||
modifier: Modifier,
|
||||
initialValue: String,
|
||||
placeholder: (@Composable () -> Unit)? = null,
|
||||
focus: Boolean = false,
|
||||
color: Color = MaterialTheme.colors.onBackground,
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
selectTextOnFocus: Boolean = false,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
onValueChange: (String) -> Unit,
|
||||
) {
|
||||
val state = remember {
|
||||
mutableStateOf(TextFieldValue(initialValue))
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!focus) return@LaunchedEffect
|
||||
focusRequester.requestFocus()
|
||||
delay(200)
|
||||
keyboard?.show()
|
||||
}
|
||||
val enabled = true
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
focusedIndicatorColor = Color.Unspecified,
|
||||
unfocusedIndicatorColor = Color.Unspecified,
|
||||
)
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
modifier = modifier
|
||||
.background(colors.backgroundColor(enabled).value, shape)
|
||||
.indicatorLine(enabled, false, interactionSource, colors)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { focusState ->
|
||||
if (focusState.isFocused && selectTextOnFocus) {
|
||||
val text = state.value.text
|
||||
state.value = state.value.copy(
|
||||
selection = TextRange(0, text.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
),
|
||||
onValueChange = {
|
||||
state.value = it
|
||||
onValueChange(it.text)
|
||||
},
|
||||
cursorBrush = SolidColor(colors.cursorColor(false).value),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboard?.hide()
|
||||
keyboardActions.onDone?.invoke(this)
|
||||
}),
|
||||
singleLine = true,
|
||||
textStyle = textStyle.copy(
|
||||
color = color,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = state.value.text,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,10 @@ package chat.simplex.app.views.newchat
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -18,8 +19,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import chat.simplex.app.views.helpers.shareText
|
||||
|
||||
@Composable
|
||||
@@ -28,6 +29,7 @@ fun AddContactView(chatModel: ChatModel) {
|
||||
if (connReq != null) {
|
||||
val cxt = LocalContext.current
|
||||
AddContactLayout(
|
||||
chatModelIncognito = chatModel.incognito.value,
|
||||
connReq = connReq,
|
||||
share = { shareText(cxt, connReq) }
|
||||
)
|
||||
@@ -35,22 +37,32 @@ fun AddContactView(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddContactLayout(connReq: String, share: () -> Unit) {
|
||||
fun AddContactLayout(chatModelIncognito: Boolean, connReq: String, share: () -> Unit) {
|
||||
BoxWithConstraints {
|
||||
val screenHeight = maxHeight
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
Modifier.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.add_contact),
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline),
|
||||
style = MaterialTheme.typography.h3,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
Row(Modifier.padding(horizontal = 8.dp)) {
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
true,
|
||||
generalGetString(R.string.incognito_random_profile_description),
|
||||
generalGetString(R.string.your_profile_will_be_sent)
|
||||
)
|
||||
}
|
||||
QRCode(
|
||||
connReq, Modifier
|
||||
.weight(1f, fill = false)
|
||||
@@ -59,14 +71,52 @@ fun AddContactLayout(connReq: String, share: () -> Unit) {
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 22.sp,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(bottom = if (screenHeight > 600.dp) 16.dp else 8.dp)
|
||||
)
|
||||
SimpleButton(stringResource(R.string.share_invitation_link), icon = Icons.Outlined.Share, click = share)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
SimpleButton(stringResource(R.string.share_invitation_link), icon = Icons.Outlined.Share, click = share)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String) {
|
||||
if (chatModelIncognito) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
if (supportedIncognito) Icons.Filled.TheaterComedy else Icons.Outlined.Info,
|
||||
stringResource(R.string.incognito),
|
||||
tint = if (supportedIncognito) Indigo else WarningOrange,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
)
|
||||
Text(onText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Info,
|
||||
stringResource(R.string.incognito),
|
||||
tint = HighOrLowlight,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
)
|
||||
Text(offText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +131,7 @@ fun AddContactLayout(connReq: String, share: () -> Unit) {
|
||||
fun PreviewAddContactView() {
|
||||
SimpleXTheme {
|
||||
AddContactLayout(
|
||||
chatModelIncognito = false,
|
||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
share = {}
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ 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.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -34,6 +35,7 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
AddGroupLayout(
|
||||
chatModel.incognito.value,
|
||||
createGroup = { groupProfile ->
|
||||
withApi {
|
||||
val groupInfo = chatModel.controller.apiNewGroup(groupProfile)
|
||||
@@ -48,7 +50,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
AddGroupMembersView(groupInfo, chatModel, close)
|
||||
AddGroupMembersView(groupInfo, chatModel, false, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +61,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
|
||||
fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val displayName = remember { mutableStateOf("") }
|
||||
@@ -92,11 +94,16 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.create_secret_group_title),
|
||||
style = MaterialTheme.typography.h4,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
)
|
||||
ReadableText(R.string.group_is_decentralized)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(stringResource(R.string.group_is_decentralized))
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
false,
|
||||
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
|
||||
generalGetString(R.string.group_main_profile_sent)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -170,6 +177,7 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
|
||||
fun PreviewAddGroupLayout() {
|
||||
SimpleXTheme {
|
||||
AddGroupLayout(
|
||||
chatModelIncognito = false,
|
||||
createGroup = {},
|
||||
close = {}
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val clipboard = getSystemService(context, ClipboardManager::class.java)
|
||||
BackHandler(onBack = close)
|
||||
PasteToConnectLayout(
|
||||
chatModel.incognito.value,
|
||||
connectionLink = connectionLink,
|
||||
pasteFromClipboard = {
|
||||
connectionLink.value = clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context) as String
|
||||
@@ -55,6 +56,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun PasteToConnectLayout(
|
||||
chatModelIncognito: Boolean,
|
||||
connectionLink: MutableState<String>,
|
||||
pasteFromClipboard: () -> Unit,
|
||||
connectViaLink: (String) -> Unit,
|
||||
@@ -62,16 +64,22 @@ fun PasteToConnectLayout(
|
||||
) {
|
||||
ModalView(close) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
Modifier.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.connect_via_link),
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
)
|
||||
Text(stringResource(R.string.paste_connection_link_below_to_connect))
|
||||
Text(stringResource(R.string.profile_will_be_sent_to_contact_sending_link))
|
||||
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
true,
|
||||
generalGetString(R.string.incognito_random_profile_from_contact_description),
|
||||
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link)
|
||||
)
|
||||
|
||||
Box(Modifier.padding(top = 16.dp, bottom = 6.dp)) {
|
||||
TextEditor(Modifier.height(180.dp), text = connectionLink)
|
||||
@@ -111,6 +119,7 @@ fun PasteToConnectLayout(
|
||||
fun PreviewPasteToConnectTextbox() {
|
||||
SimpleXTheme {
|
||||
PasteToConnectLayout(
|
||||
chatModelIncognito = false,
|
||||
connectionLink = remember { mutableStateOf("") },
|
||||
pasteFromClipboard = {},
|
||||
connectViaLink = { link ->
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -22,6 +21,7 @@ import chat.simplex.app.views.helpers.*
|
||||
fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
ConnectContactLayout(
|
||||
chatModelIncognito = chatModel.incognito.value,
|
||||
qrCodeScanner = {
|
||||
QRCodeScanner { connReqUri ->
|
||||
try {
|
||||
@@ -67,21 +67,22 @@ suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) {
|
||||
fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit, close: () -> Unit) {
|
||||
ModalView(close) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
Modifier.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
generalGetString(R.string.scan_QR_code),
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
modifier = Modifier.padding(vertical = 5.dp)
|
||||
)
|
||||
Text(
|
||||
generalGetString(R.string.your_chat_profile_will_be_sent_to_your_contact),
|
||||
style = MaterialTheme.typography.h3,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
true,
|
||||
generalGetString(R.string.incognito_random_profile_description),
|
||||
generalGetString(R.string.your_profile_will_be_sent)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
@@ -106,6 +107,7 @@ fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Uni
|
||||
fun PreviewConnectContactLayout() {
|
||||
SimpleXTheme {
|
||||
ConnectContactLayout(
|
||||
chatModelIncognito = false,
|
||||
qrCodeScanner = { Surface {} },
|
||||
close = {},
|
||||
)
|
||||
|
||||
@@ -3,11 +3,15 @@ package chat.simplex.app.views.usersettings
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
@@ -82,6 +86,40 @@ fun SharedPreferenceToggle(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SharedPreferenceToggleWithIcon(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
stopped: Boolean = false,
|
||||
onClickInfo: () -> Unit,
|
||||
preference: Preference<Boolean>,
|
||||
preferenceState: MutableState<Boolean>? = null
|
||||
) {
|
||||
val prefState = preferenceState ?: remember { mutableStateOf(preference.get()) }
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text, Modifier.padding(end = 4.dp))
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
Modifier.clickable(onClick = onClickInfo),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Switch(
|
||||
checked = prefState.value,
|
||||
onCheckedChange = {
|
||||
preference.set(it)
|
||||
prefState.value = it
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
),
|
||||
enabled = !stopped
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T>SharedPreferenceRadioButton(text: String, prefState: MutableState<T>, preference: Preference<T>, value: T) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun IncognitoView() {
|
||||
IncognitoLayout()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoLayout() {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.settings_section_title_incognito),
|
||||
Modifier.padding(start = 8.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
Text(generalGetString(R.string.incognito_info_protects))
|
||||
Text(generalGetString(R.string.incognito_info_allows))
|
||||
Text(generalGetString(R.string.incognito_info_share))
|
||||
Text(generalGetString(R.string.incognito_info_find))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -37,6 +37,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
MaintainIncognitoState(chatModel)
|
||||
|
||||
fun setRunServiceInBackground(on: Boolean) {
|
||||
chatModel.controller.appPrefs.runServiceInBackground.set(on)
|
||||
if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
|
||||
@@ -51,6 +53,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
SettingsLayout(
|
||||
profile = user.profile,
|
||||
stopped,
|
||||
chatModel.incognito,
|
||||
chatModel.controller.appPrefs.incognito,
|
||||
runServiceInBackground = chatModel.runServiceInBackground,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools,
|
||||
setRunServiceInBackground = ::setRunServiceInBackground,
|
||||
@@ -84,8 +88,10 @@ val simplexTeamUri =
|
||||
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
profile: Profile,
|
||||
profile: LocalProfile,
|
||||
stopped: Boolean,
|
||||
incognito: MutableState<Boolean>,
|
||||
incognitoPref: Preference<Boolean>,
|
||||
runServiceInBackground: MutableState<Boolean>,
|
||||
developerTools: Preference<Boolean>,
|
||||
setRunServiceInBackground: (Boolean) -> Unit,
|
||||
@@ -116,6 +122,8 @@ fun SettingsLayout(
|
||||
ProfilePreview(profile, stopped = stopped)
|
||||
}
|
||||
SectionDivider()
|
||||
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { onClickIncognitoInfo(showModal) }
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
DatabaseItem(showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
@@ -163,6 +171,47 @@ fun SettingsLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsIncognitoActionItem(
|
||||
incognitoPref: Preference<Boolean>,
|
||||
incognito: MutableState<Boolean>,
|
||||
stopped: Boolean,
|
||||
onClickInfo: () -> Unit,
|
||||
) {
|
||||
SettingsPreferenceItemWithInfo(
|
||||
if (incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.TheaterComedy,
|
||||
if (incognito.value) Indigo else HighOrLowlight,
|
||||
stringResource(R.string.incognito),
|
||||
stopped,
|
||||
onClickInfo,
|
||||
incognitoPref,
|
||||
incognito
|
||||
)
|
||||
}
|
||||
|
||||
private val onClickIncognitoInfo: ((@Composable (ChatModel) -> Unit) -> (() -> Unit)) -> Unit = { showModal ->
|
||||
showModal { IncognitoView() }()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
// Cache previous value and once it changes in background, update it via API
|
||||
var cachedIncognito by remember { mutableStateOf(chatModel.incognito.value) }
|
||||
LaunchedEffect(chatModel.incognito.value) {
|
||||
// Don't do anything if nothing changed
|
||||
if (cachedIncognito == chatModel.incognito.value) return@LaunchedEffect
|
||||
try {
|
||||
chatModel.controller.apiSetIncognito(chatModel.incognito.value)
|
||||
} catch (e: Exception) {
|
||||
// Rollback the state
|
||||
chatModel.controller.appPrefs.incognito.set(cachedIncognito)
|
||||
// Crash the app
|
||||
throw e
|
||||
}
|
||||
cachedIncognito = chatModel.incognito.value
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun DatabaseItem(openDatabaseView: () -> Unit, stopped: Boolean) {
|
||||
SectionItemView(openDatabaseView) {
|
||||
Row(
|
||||
@@ -322,6 +371,25 @@ fun SettingsPreferenceItem(icon: ImageVector, text: String, pref: Preference<Boo
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsPreferenceItemWithInfo(
|
||||
icon: ImageVector,
|
||||
iconTint: Color,
|
||||
text: String,
|
||||
stopped: Boolean,
|
||||
onClickInfo: () -> Unit,
|
||||
pref: Preference<Boolean>,
|
||||
prefState: MutableState<Boolean>? = null
|
||||
) {
|
||||
SectionItemView() {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onClickInfo() }) {
|
||||
Icon(icon, text, tint = if (stopped) HighOrLowlight else iconTint)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
SharedPreferenceToggleWithIcon(text, Icons.Outlined.Info, stopped, onClickInfo, pref, prefState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
@@ -332,8 +400,10 @@ fun SettingsPreferenceItem(icon: ImageVector, text: String, pref: Preference<Boo
|
||||
fun PreviewSettingsLayout() {
|
||||
SimpleXTheme {
|
||||
SettingsLayout(
|
||||
profile = Profile.sampleData,
|
||||
profile = LocalProfile.sampleData,
|
||||
stopped = false,
|
||||
incognito = remember { mutableStateOf(false) },
|
||||
incognitoPref = Preference({ false}, {}),
|
||||
runServiceInBackground = remember { mutableStateOf(true) },
|
||||
developerTools = Preference({ false }, {}),
|
||||
setRunServiceInBackground = {},
|
||||
|
||||
+5
-4
@@ -22,8 +22,7 @@ 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.ChatModel
|
||||
import chat.simplex.app.model.Profile
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
@@ -37,7 +36,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
val editProfile = remember { mutableStateOf(false) }
|
||||
var profile by remember { mutableStateOf(user.profile) }
|
||||
var profile by remember { mutableStateOf(user.profile.toProfile()) }
|
||||
UserProfileLayout(
|
||||
close = close,
|
||||
editProfile = editProfile,
|
||||
@@ -47,7 +46,9 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val p = Profile(displayName, fullName, image)
|
||||
val newProfile = chatModel.controller.apiUpdateProfile(p)
|
||||
if (newProfile != null) {
|
||||
chatModel.updateUserProfile(newProfile)
|
||||
chatModel.currentUser.value?.profile?.profileId?.let {
|
||||
chatModel.updateUserProfile(newProfile.toLocalProfile(it))
|
||||
}
|
||||
profile = newProfile
|
||||
}
|
||||
editProfile.value = false
|
||||
|
||||
@@ -31,8 +31,11 @@
|
||||
<string name="display_name_invited_to_connect">приглашение соединиться</string>
|
||||
<string name="display_name_connecting">соединяется…</string>
|
||||
<string name="description_you_shared_one_time_link">вы создали одноразовую ссылку</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">вы создали одноразовую ссылку инкогнито</string>
|
||||
<string name="description_via_contact_address_link">через ссылку-контакт</string>
|
||||
<string name="description_via_contact_address_link_incognito">инкогнито через ссылку-контакт</string>
|
||||
<string name="description_via_one_time_link">через одноразовую ссылку</string>
|
||||
<string name="description_via_one_time_link_incognito">инкогнито через одноразовую ссылку</string>
|
||||
|
||||
<!-- SimpleXAPI.kt -->
|
||||
<string name="error_saving_smp_servers">Ошибка при сохранении SMP серверов</string>
|
||||
@@ -114,6 +117,7 @@
|
||||
<string name="your_chats">Ваши чаты</string>
|
||||
<string name="contact_connection_pending">соединяется…</string>
|
||||
<string name="group_preview_you_are_invited">вы приглашены в группу</string>
|
||||
<string name="group_preview_join_as">вступить как %s</string>
|
||||
<string name="group_connection_pending">соединяется…</string>
|
||||
|
||||
<!-- ComposeView.kt, helpers -->
|
||||
@@ -148,6 +152,7 @@
|
||||
<string name="delete_contact_question">Удалить контакт?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Контакт и все сообщения будут удалены - это действие нельзя отменить!</string>
|
||||
<string name="button_delete_contact">Удалить контакт</string>
|
||||
<string name="text_field_set_contact_placeholder">Имя контакта…</string>
|
||||
<string name="icon_descr_server_status_connected">Соединение с сервером установлено</string>
|
||||
<string name="icon_descr_server_status_disconnected">Соединение с сервером не установлено</string>
|
||||
<string name="icon_descr_server_status_error">Ошибка соединения с сервером</string>
|
||||
@@ -160,7 +165,7 @@
|
||||
<string name="back">Назад</string>
|
||||
<string name="cancel_verb">Отменить</string>
|
||||
<string name="confirm_verb">Подтвердить</string>
|
||||
<string name="ok">Oк</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">нет описания</string>
|
||||
<string name="add_contact">Добавить контакт</string>
|
||||
<string name="copied">Скопировано в буфер обмена</string>
|
||||
@@ -199,6 +204,7 @@
|
||||
<string name="accept_connection_request__question">Принять запрос на соединение?</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Отправителю НЕ будет послано уведомление, если вы отклоните запрос на соединение.</string>
|
||||
<string name="accept_contact_button">Принять</string>
|
||||
<string name="accept_contact_incognito_button">Принять инкогнито</string>
|
||||
<string name="reject_contact_button">Отклонить</string>
|
||||
|
||||
<!-- Clear Chat - ChatListNavLinkView.kt -->
|
||||
@@ -251,13 +257,15 @@
|
||||
<string name="connection_request_sent">Запрос на соединение послан!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Соединение будет установлено когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Соединение будет установлено когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Покажите QR код вашему контакту, чтобы сосканировать его из приложения</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Покажите QR код вашему контакту, чтобы сосканировать его из приложения.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Если вы не можете встретиться лично, вы можете <b>показать QR код во время видеозвонка</b> или отправить ссылку через любой другой канал связи.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Ваш профиль будет отправлен\nвашему контакту</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Если вы не можете встретиться лично, вы можете <b>сосканировать QR код во время видеозвонка</b>, или ваш контакт может отправить вам ссылку.</string>
|
||||
<string name="share_invitation_link">Поделиться ссылкой</string>
|
||||
<string name="paste_connection_link_below_to_connect">Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта.</string>
|
||||
|
||||
<string name="your_profile_will_be_sent">Ваш профиль будет отправлен вашему контакту</string>
|
||||
|
||||
|
||||
<!-- settings - SettingsView.kt -->
|
||||
<string name="your_settings">Настройки</string>
|
||||
<string name="your_simplex_contact_address">Ваш <xliff:g id="appName">SimpleX</xliff:g> адрес</string>
|
||||
@@ -469,6 +477,7 @@
|
||||
<string name="settings_section_title_socks">SOCKS ПРОКСИ</string>
|
||||
<string name="settings_section_title_icon">ИКОНКА</string>
|
||||
<string name="settings_section_title_themes">ТЕМЫ</string>
|
||||
<string name="settings_section_title_incognito">Режим Инкогнито</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Данные чата</string>
|
||||
@@ -519,6 +528,7 @@
|
||||
<string name="join_group_question">Вступить в группу?</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Вы приглашены в группу. Вступите, чтобы соединиться с членами группы.</string>
|
||||
<string name="join_group_button">Вступить</string>
|
||||
<string name="join_group_incognito_button">Вступить инкогнито</string>
|
||||
<string name="joining_group">Вступление в группу</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы.</string>
|
||||
<string name="leave_group_button">Выйти</string>
|
||||
@@ -531,11 +541,14 @@
|
||||
<string name="alert_title_no_group">Группа не найдена!</string>
|
||||
<string name="alert_message_no_group">Эта группа больше не существует.</string>
|
||||
<string name="alert_title_join_group_error">Ошибка приглашения</string>
|
||||
<string name="alert_title_cant_invite_contacts">Нельзя пригласить контакты!</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено</string>
|
||||
|
||||
<!-- CIGroupInvitationView.kt -->
|
||||
<string name="you_sent_group_invitation">Вы отправили приглашение в группу</string>
|
||||
<string name="you_are_invited_to_group">Вы приглашены в группу</string>
|
||||
<string name="group_invitation_tap_to_join">Нажмите, чтобы вступить</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Нажмите, чтобы вступить инкогнито</string>
|
||||
<string name="you_joined_this_group">Вы вступили в эту группу</string>
|
||||
<string name="you_rejected_group_invitation">Вы отклонили приглашение в группу</string>
|
||||
<string name="group_invitation_expired">Приглашение в группу истекло</string>
|
||||
@@ -581,6 +594,8 @@
|
||||
<string name="clear_contacts_selection_button">Очистить</string>
|
||||
<string name="num_contacts_selected">Выбрано контактов: <xliff:g id="num_contacts">%1$s</xliff:g></string>
|
||||
<string name="no_contacts_selected">Контакты не выбраны</string>
|
||||
<string name="invite_prohibited">Нельзя пригласить контакт!</string>
|
||||
<string name="invite_prohibited_description">Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль</string>
|
||||
|
||||
<!-- GroupChatInfoView.kt -->
|
||||
<string name="button_add_members">Пригласить членов группы</string>
|
||||
@@ -618,6 +633,8 @@
|
||||
<string name="group_is_decentralized">Группа полностью децентрализована — она видна только членам.</string>
|
||||
<string name="group_display_name_field">Имя группы:</string>
|
||||
<string name="group_full_name_field">Полное имя:</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы</string>
|
||||
<string name="group_main_profile_sent">Ваш профиль чата будет отправлен членам группы</string>
|
||||
|
||||
<!-- GroupProfileView.kt -->
|
||||
<string name="group_profile_is_stored_on_members_devices">Профиль группы хранится на устройствах членов, а не на серверах.</string>
|
||||
@@ -637,6 +654,17 @@
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Обновление настроек приведет к переподключению клиента ко всем серверам.</string>
|
||||
<string name="update_network_settings_confirmation">Обновить</string>
|
||||
|
||||
<!-- Incognito mode -->
|
||||
<string name="incognito">Инкогнито</string>
|
||||
<string name="incognito_random_profile">Случайный профиль</string>
|
||||
<string name="incognito_random_profile_description">Вашему контакту будет отправлен случайный профиль</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль</string>
|
||||
|
||||
<string name="incognito_info_protects">Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль.</string>
|
||||
<string name="incognito_info_allows">Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя.</string>
|
||||
<string name="incognito_info_share">Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.</string>
|
||||
<string name="incognito_info_find">Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата.</string>
|
||||
|
||||
<!-- Default themes -->
|
||||
<string name="theme_system">Системная</string>
|
||||
<string name="theme_light">Светлая</string>
|
||||
|
||||
@@ -31,8 +31,11 @@
|
||||
<string name="display_name_invited_to_connect">invited to connect</string>
|
||||
<string name="display_name_connecting">connecting…</string>
|
||||
<string name="description_you_shared_one_time_link">you shared one-time link</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">you shared one-time link incognito</string>
|
||||
<string name="description_via_contact_address_link">via contact address link</string>
|
||||
<string name="description_via_contact_address_link_incognito">incognito via contact address link</string>
|
||||
<string name="description_via_one_time_link">via one-time link</string>
|
||||
<string name="description_via_one_time_link_incognito">incognito via one-time link</string>
|
||||
|
||||
<!-- SimpleXAPI.kt -->
|
||||
<string name="error_saving_smp_servers">Error saving SMP servers</string>
|
||||
@@ -114,6 +117,7 @@
|
||||
<string name="your_chats">Your chats</string>
|
||||
<string name="contact_connection_pending">connecting…</string>
|
||||
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||
<string name="group_preview_join_as">join as %s</string>
|
||||
<string name="group_connection_pending">connecting…</string>
|
||||
|
||||
<!-- ComposeView.kt, helpers -->
|
||||
@@ -148,6 +152,7 @@
|
||||
<string name="delete_contact_question">Delete contact?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string>
|
||||
<string name="button_delete_contact">Delete contact</string>
|
||||
<string name="text_field_set_contact_placeholder">Set contact name…</string>
|
||||
<string name="icon_descr_server_status_connected">Connected</string>
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
<string name="icon_descr_server_status_error">Error</string>
|
||||
@@ -160,7 +165,7 @@
|
||||
<string name="back">Back</string>
|
||||
<string name="cancel_verb">Cancel</string>
|
||||
<string name="confirm_verb">Confirm</string>
|
||||
<string name="ok">Ok</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">no details</string>
|
||||
<string name="add_contact">Add contact</string>
|
||||
<string name="copied">Copied to clipboard</string>
|
||||
@@ -199,6 +204,7 @@
|
||||
<string name="accept_connection_request__question">Accept connection request?</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">If you choose to reject sender will NOT be notified.</string>
|
||||
<string name="accept_contact_button">Accept</string>
|
||||
<string name="accept_contact_incognito_button">Accept incognito</string>
|
||||
<string name="reject_contact_button">Reject</string>
|
||||
|
||||
<!-- Clear Chat - ChatListNavLinkView.kt -->
|
||||
@@ -251,12 +257,13 @@
|
||||
<string name="connection_request_sent">Connection request sent!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">You will be connected when your connection request is accepted, please wait or check later!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">You will be connected when your contact\'s device is online, please wait or check later!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Show QR code for your contact\nto scan from the app</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">If you cannot meet in person, you can <b>show QR code in the video call</b>, or you can share the invitation link via any other channel.</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Show QR code for your contact to scan from the app.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">If you can\'t meet in person, you can <b>show QR code in the video call</b>, or you can share the invitation link via any other channel.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Your chat profile will be sent\nto your contact</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">If you cannot meet in person, you can <b>scan QR code in the video call</b>, or your contact can share an invitation link.</string>
|
||||
<string name="share_invitation_link">Share invitation link</string>
|
||||
<string name="paste_connection_link_below_to_connect">Paste the link you received into the box below to connect with your contact.</string>
|
||||
<string name="your_profile_will_be_sent">Your chat profile will be sent to your contact</string>
|
||||
|
||||
<!-- PasteToConnect.kt -->
|
||||
<string name="connect_via_link">Connect via link</string>
|
||||
@@ -471,6 +478,7 @@
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_incognito">Incognito mode</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Your chat database</string>
|
||||
@@ -521,6 +529,7 @@
|
||||
<string name="join_group_question">Join group?</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">You are invited to group. Join to connect with group members.</string>
|
||||
<string name="join_group_button">Join</string>
|
||||
<string name="join_group_incognito_button">Join incognito</string>
|
||||
<string name="joining_group">Joining group</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">You joined this group. Connecting to inviting group member.</string>
|
||||
<string name="leave_group_button">Leave</string>
|
||||
@@ -533,11 +542,14 @@
|
||||
<string name="alert_title_no_group">Group not found!</string>
|
||||
<string name="alert_message_no_group">This group no longer exists.</string>
|
||||
<string name="alert_title_join_group_error">Error joining group</string>
|
||||
<string name="alert_title_cant_invite_contacts">Can\'t invite contacts!</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">You\'re using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</string>
|
||||
|
||||
<!-- CIGroupInvitationView.kt -->
|
||||
<string name="you_sent_group_invitation">You sent group invitation</string>
|
||||
<string name="you_are_invited_to_group">You are invited to group</string>
|
||||
<string name="group_invitation_tap_to_join">Tap to join</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Tap to join incognito</string>
|
||||
<string name="you_joined_this_group">You joined this group</string>
|
||||
<string name="you_rejected_group_invitation">You rejected group invitation</string>
|
||||
<string name="group_invitation_expired">Group invitation expired</string>
|
||||
@@ -583,6 +595,8 @@
|
||||
<string name="clear_contacts_selection_button">Clear</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contact(s) selected</string>
|
||||
<string name="no_contacts_selected">No contacts selected</string>
|
||||
<string name="invite_prohibited">Can\'t invite contact!</string>
|
||||
<string name="invite_prohibited_description">You\'re trying to invite contact with whom you\'ve shared an incognito profile to the group in which you\'re using your main profile</string>
|
||||
|
||||
<!-- GroupChatInfoView.kt -->
|
||||
<string name="button_add_members">Invite members</string>
|
||||
@@ -620,6 +634,9 @@
|
||||
<string name="group_is_decentralized">The group is fully decentralized – it is visible only to the members.</string>
|
||||
<string name="group_display_name_field">Group display name:</string>
|
||||
<string name="group_full_name_field">Group full name:</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Incognito mode is not supported here - your main profile will be sent to group members</string>
|
||||
<string name="group_main_profile_sent">Your chat profile will be sent to group members</string>
|
||||
|
||||
|
||||
<!-- GroupProfileView.kt -->
|
||||
<string name="group_profile_is_stored_on_members_devices">Group profile is stored on members\' devices, not on the servers.</string>
|
||||
@@ -639,6 +656,17 @@
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Updating settings will re-connect the client to all servers.</string>
|
||||
<string name="update_network_settings_confirmation">Update</string>
|
||||
|
||||
<!-- Incognito mode -->
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="incognito_random_profile">Your random profile</string>
|
||||
<string name="incognito_random_profile_description">A random profile will be sent to your contact</string>
|
||||
<string name="incognito_random_profile_from_contact_description">A random profile will be sent to the contact that you received this link from</string>
|
||||
|
||||
<string name="incognito_info_protects">Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</string>
|
||||
<string name="incognito_info_allows">It allows having many anonymous connections without any shared data between them in a single chat profile.</string>
|
||||
<string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string>
|
||||
<string name="incognito_info_find">To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</string>
|
||||
|
||||
<!-- Default themes -->
|
||||
<string name="theme_system">System</string>
|
||||
<string name="theme_light">Light</string>
|
||||
|
||||
@@ -59,6 +59,16 @@ final class ChatModel: ObservableObject {
|
||||
chats.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
func getContactChat(_ contactId: Int64) -> Chat? {
|
||||
chats.first { chat in
|
||||
if case let .direct(contact) = chat.chatInfo {
|
||||
return contact.contactId == contactId
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getChatIndex(_ id: String) -> Int? {
|
||||
chats.firstIndex(where: { $0.id == id })
|
||||
}
|
||||
|
||||
@@ -206,8 +206,9 @@ func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, sear
|
||||
func loadChat(chat: Chat, search: String = "") {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
|
||||
let m = ChatModel.shared
|
||||
m.reversedChatItems = []
|
||||
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search)
|
||||
m.updateChatInfo(chat.chatInfo)
|
||||
m.reversedChatItems = chat.chatItems.reversed()
|
||||
} catch let error {
|
||||
@@ -324,9 +325,9 @@ func apiContactInfo(contactId: Int64) async throws -> (ConnectionStats?, Profile
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?, LocalProfile?) {
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?) {
|
||||
let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, connStats_, localMainProfile) = r { return (connStats_, localMainProfile) }
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
|
||||
throw r
|
||||
}
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ struct ChatInfoView: View {
|
||||
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId)
|
||||
await MainActor.run {
|
||||
chatModel.removeChat(chat.chatInfo.id)
|
||||
chatModel.chatId = nil
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
|
||||
@@ -20,7 +20,6 @@ struct CIGroupInvitationView: View {
|
||||
|
||||
var body: some View {
|
||||
let action = !chatItem.chatDir.sent && groupInvitation.status == .pending
|
||||
let unsafeToJoinIncognito = interactiveIncognito && !chatIncognito
|
||||
let v = ZStack(alignment: .bottomTrailing) {
|
||||
VStack(alignment: .leading) {
|
||||
groupInfoView(action)
|
||||
@@ -34,8 +33,8 @@ struct CIGroupInvitationView: View {
|
||||
if action {
|
||||
groupInvitationText()
|
||||
.overlay(DetermineWidth())
|
||||
Text(interactiveIncognito ? "Tap to join incognito" : "Tap to join")
|
||||
.foregroundColor(interactiveIncognito ? .indigo : .accentColor)
|
||||
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
||||
.foregroundColor(chatIncognito ? .indigo : .accentColor)
|
||||
.font(.callout)
|
||||
.padding(.trailing, 60)
|
||||
.overlay(DetermineWidth())
|
||||
@@ -59,25 +58,17 @@ struct CIGroupInvitationView: View {
|
||||
|
||||
if action {
|
||||
v.onTapGesture {
|
||||
if unsafeToJoinIncognito {
|
||||
AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(groupInvitation.groupId))
|
||||
} else {
|
||||
joinGroup(groupInvitation.groupId)
|
||||
}
|
||||
joinGroup(groupInvitation.groupId)
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
private var interactiveIncognito: Bool {
|
||||
(groupInvitation.invitedIncognito ?? false) || chatModel.incognito
|
||||
}
|
||||
|
||||
private func groupInfoView(_ action: Bool) -> some View {
|
||||
var color: Color
|
||||
if action {
|
||||
color = interactiveIncognito ? .indigo : .accentColor
|
||||
color = chatIncognito ? .indigo : .accentColor
|
||||
} else {
|
||||
color = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
@@ -107,7 +98,7 @@ struct CIGroupInvitationView: View {
|
||||
|
||||
private func groupInvitationStr() -> LocalizedStringKey {
|
||||
if chatItem.chatDir.sent {
|
||||
return (groupInvitation.invitedIncognito ?? false) ? "You sent group invitation incognito" : "You sent group invitation"
|
||||
return "You sent group invitation"
|
||||
} else {
|
||||
switch groupInvitation.status {
|
||||
case .pending: return "You are invited to group"
|
||||
|
||||
@@ -34,7 +34,6 @@ struct ChatView: View {
|
||||
// opening GroupMemberInfoView on member icon
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@State private var memberConnectionStats: ConnectionStats?
|
||||
@State private var memberMainProfile: LocalProfile?
|
||||
|
||||
var body: some View {
|
||||
let cInfo = chat.chatInfo
|
||||
@@ -73,7 +72,10 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.backward")
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "chevron.backward")
|
||||
Text("Chats")
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
@@ -137,10 +139,16 @@ struct ChatView: View {
|
||||
case let .group(groupInfo):
|
||||
HStack {
|
||||
if groupInfo.canAddMembers {
|
||||
addMembersButton()
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
if (chat.chatInfo.incognito) {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.onTapGesture { AlertManager.shared.showAlert(cantInviteIncognitoAlert()) }
|
||||
} else {
|
||||
addMembersButton()
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
searchButton()
|
||||
@@ -230,6 +238,15 @@ struct ChatView: View {
|
||||
.onChange(of: searchText) { _ in
|
||||
loadChat(chat: chat, search: searchText)
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
showChatInfoSheet = false
|
||||
loadChat(chat: chat)
|
||||
DispatchQueue.main.async {
|
||||
scrollToBottom(proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||
@@ -360,22 +377,16 @@ struct ChatView: View {
|
||||
.onTapGesture {
|
||||
Task {
|
||||
do {
|
||||
let (stats, profile) = try await apiGroupMemberInfo(member.groupId, member.groupMemberId)
|
||||
await MainActor.run {
|
||||
memberConnectionStats = stats
|
||||
memberMainProfile = profile
|
||||
}
|
||||
let stats = try await apiGroupMemberInfo(member.groupId, member.groupMemberId)
|
||||
await MainActor.run { memberConnectionStats = stats }
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo error: \(responseError(error))")
|
||||
}
|
||||
await MainActor.run { selectedMember = member }
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedMember, onDismiss: {
|
||||
memberConnectionStats = nil
|
||||
memberMainProfile = nil
|
||||
}) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats, mainProfile: memberMainProfile)
|
||||
.sheet(item: $selectedMember, onDismiss: { memberConnectionStats = nil }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats)
|
||||
}
|
||||
} else {
|
||||
Rectangle().fill(.clear)
|
||||
|
||||
@@ -15,6 +15,7 @@ struct AddGroupMembersView: View {
|
||||
var chat: Chat
|
||||
var groupInfo: GroupInfo
|
||||
var showSkip: Bool = false
|
||||
var showFooterCounter: Bool = true
|
||||
var addedMembersCb: ((Set<Int64>) -> Void)? = nil
|
||||
@State private var selectedContacts = Set<Int64>()
|
||||
@State private var selectedRole: GroupMemberRole = .admin
|
||||
@@ -22,13 +23,11 @@ struct AddGroupMembersView: View {
|
||||
|
||||
private enum AddGroupMembersAlert: Identifiable {
|
||||
case prohibitedToInviteIncognito
|
||||
case warnUnsafeToInviteIncognito
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .prohibitedToInviteIncognito: return "prohibitedToInviteIncognito"
|
||||
case .warnUnsafeToInviteIncognito: return "warnUnsafeToInviteIncognito"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -37,10 +36,6 @@ struct AddGroupMembersView: View {
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
let membersToAdd = filterMembersToAdd(chatModel.groupMembers)
|
||||
let nonIncognitoConnectionsSelected = membersToAdd
|
||||
.filter{ selectedContacts.contains($0.apiId) }
|
||||
.contains(where: { !$0.contactConnIncognito })
|
||||
let unsafeToInviteIncognito = chat.chatInfo.incognito && nonIncognitoConnectionsSelected
|
||||
|
||||
let v = List {
|
||||
ChatInfoToolbar(chat: chat, imageSize: 48)
|
||||
@@ -58,18 +53,20 @@ struct AddGroupMembersView: View {
|
||||
let count = selectedContacts.count
|
||||
Section {
|
||||
rolePicker()
|
||||
inviteMembersButton(unsafeToInviteIncognito)
|
||||
inviteMembersButton()
|
||||
.disabled(count < 1)
|
||||
} footer: {
|
||||
if (count >= 1) {
|
||||
HStack {
|
||||
Button { selectedContacts.removeAll() } label: { Text("Clear") }
|
||||
Spacer()
|
||||
Text("\(count) contact(s) selected")
|
||||
if showFooterCounter {
|
||||
if (count >= 1) {
|
||||
HStack {
|
||||
Button { selectedContacts.removeAll() } label: { Text("Clear") }
|
||||
Spacer()
|
||||
Text("\(count) contact(s) selected")
|
||||
}
|
||||
} else {
|
||||
Text("No contacts selected")
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
} else {
|
||||
Text("No contacts selected")
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,27 +100,15 @@ struct AddGroupMembersView: View {
|
||||
title: Text("Can't invite contact!"),
|
||||
message: Text("You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile")
|
||||
)
|
||||
case .warnUnsafeToInviteIncognito:
|
||||
return Alert(
|
||||
title: Text("Your main profile may be shared"),
|
||||
message: Text("Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members."),
|
||||
primaryButton: .destructive(Text("Invite anyway")) {
|
||||
inviteMembers()
|
||||
}, secondaryButton: .cancel()
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text("\(error)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func inviteMembersButton(_ unsafeToInviteIncognito: Bool) -> some View {
|
||||
private func inviteMembersButton() -> some View {
|
||||
Button {
|
||||
if unsafeToInviteIncognito {
|
||||
alert = .warnUnsafeToInviteIncognito
|
||||
} else {
|
||||
inviteMembers()
|
||||
}
|
||||
inviteMembers()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Invite to group")
|
||||
@@ -161,7 +146,6 @@ struct AddGroupMembersView: View {
|
||||
private func contactCheckView(_ contact: Contact) -> some View {
|
||||
let checked = selectedContacts.contains(contact.apiId)
|
||||
let prohibitedToInviteIncognito = !chat.chatInfo.incognito && contact.contactConnIncognito
|
||||
let safeToInviteIncognito = chat.chatInfo.incognito && contact.contactConnIncognito
|
||||
var icon: String
|
||||
var iconColor: Color
|
||||
if prohibitedToInviteIncognito {
|
||||
@@ -195,11 +179,6 @@ struct AddGroupMembersView: View {
|
||||
.foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
if safeToInviteIncognito {
|
||||
Image(systemName: "theatermasks")
|
||||
.foregroundColor(.indigo)
|
||||
.font(.footnote)
|
||||
}
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(iconColor)
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ struct GroupChatInfoView: View {
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@State private var showGroupProfile: Bool = false
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var memberMainProfile: LocalProfile?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
enum GroupChatInfoViewAlert: Identifiable {
|
||||
case deleteGroupAlert
|
||||
case clearChatAlert
|
||||
case leaveGroupAlert
|
||||
case cantInviteIncognitoAlert
|
||||
|
||||
var id: GroupChatInfoViewAlert { get { self } }
|
||||
}
|
||||
@@ -43,18 +43,21 @@ struct GroupChatInfoView: View {
|
||||
|
||||
Section("\(members.count + 1) members") {
|
||||
if groupInfo.canAddMembers {
|
||||
addMembersButton()
|
||||
if (chat.chatInfo.incognito) {
|
||||
Label("Invite members", systemImage: "plus")
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.onTapGesture { alert = .cantInviteIncognitoAlert }
|
||||
} else {
|
||||
addMembersButton()
|
||||
}
|
||||
}
|
||||
memberView(groupInfo.membership, user: true)
|
||||
ForEach(members) { member in
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
let (stats, profile) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
await MainActor.run {
|
||||
connectionStats = stats
|
||||
memberMainProfile = profile
|
||||
}
|
||||
let stats = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
await MainActor.run { connectionStats = stats }
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo error: \(responseError(error))")
|
||||
}
|
||||
@@ -66,11 +69,8 @@ struct GroupChatInfoView: View {
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
.sheet(item: $selectedMember, onDismiss: {
|
||||
connectionStats = nil
|
||||
memberMainProfile = nil
|
||||
}) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats, mainProfile: memberMainProfile)
|
||||
.sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats)
|
||||
}
|
||||
.sheet(isPresented: $showGroupProfile) {
|
||||
GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile)
|
||||
@@ -104,6 +104,8 @@ struct GroupChatInfoView: View {
|
||||
case .deleteGroupAlert: return deleteGroupAlert()
|
||||
case .clearChatAlert: return clearChatAlert()
|
||||
case .leaveGroupAlert: return leaveGroupAlert()
|
||||
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,6 +221,7 @@ struct GroupChatInfoView: View {
|
||||
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId)
|
||||
await MainActor.run {
|
||||
chatModel.removeChat(chat.chatInfo.id)
|
||||
chatModel.chatId = nil
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
@@ -259,6 +262,13 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func cantInviteIncognitoAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Can't invite contacts!"),
|
||||
message: Text("You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed")
|
||||
)
|
||||
}
|
||||
|
||||
struct GroupChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData)
|
||||
|
||||
@@ -15,7 +15,6 @@ struct GroupMemberInfoView: View {
|
||||
var groupInfo: GroupInfo
|
||||
var member: GroupMember
|
||||
var connectionStats: ConnectionStats?
|
||||
var mainProfile: LocalProfile?
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
@@ -31,17 +30,14 @@ struct GroupMemberInfoView: View {
|
||||
groupMemberInfoHeader()
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
// if let contactId = member.memberContactId {
|
||||
// Section {
|
||||
// openDirectChatButton(contactId)
|
||||
// }
|
||||
// }
|
||||
if let contactId = member.memberContactId {
|
||||
Section {
|
||||
openDirectChatButton(contactId)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
if let mainProfile = mainProfile {
|
||||
mainProfileRow(mainProfile)
|
||||
}
|
||||
// TODO change role
|
||||
// localizedInfoRow("Role", member.memberRole.text)
|
||||
// TODO invited by - need to get contact by contact id
|
||||
@@ -84,32 +80,25 @@ struct GroupMemberInfoView: View {
|
||||
|
||||
func openDirectChatButton(_ contactId: Int64) -> some View {
|
||||
Button {
|
||||
if let i = chatModel.chats.firstIndex(where: { chat in
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact): return contact.contactId == contactId
|
||||
default: return false
|
||||
var chat = chatModel.getContactChat(contactId)
|
||||
if chat == nil {
|
||||
do {
|
||||
chat = try apiGetChat(type: .direct, id: contactId)
|
||||
if let chat = chat {
|
||||
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
|
||||
chat.serverInfo = Chat.ServerInfo(networkStatus: .connected)
|
||||
chatModel.addChat(chat)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
|
||||
}
|
||||
}) {
|
||||
}
|
||||
if let chat = chat {
|
||||
dismissAllSheets(animated: true)
|
||||
chatModel.chatId = chatModel.chats[i].chatInfo.id
|
||||
chatModel.chatId = chat.id
|
||||
}
|
||||
} label: {
|
||||
Label("Send direct message", systemImage: "message")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
private func mainProfileRow(_ mainProfile: LocalProfile) -> some View {
|
||||
HStack {
|
||||
Text("Known main profile")
|
||||
Spacer()
|
||||
if (mainProfile.image != nil) {
|
||||
ProfileImage(imageStr: member.image)
|
||||
.frame(width: 38, height: 38)
|
||||
.padding(.trailing, 2)
|
||||
}
|
||||
Text(mainProfile.chatViewName)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,16 +28,10 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func chatView() -> some View {
|
||||
ChatView(chat: chat)
|
||||
.onAppear { loadChat(chat: chat) }
|
||||
}
|
||||
|
||||
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
|
||||
let v = NavLinkPlain(
|
||||
tag: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
destination: { chatView() },
|
||||
label: { ChatPreviewView(chat: chat) },
|
||||
disabled: !contact.ready
|
||||
)
|
||||
@@ -82,12 +76,8 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.onTapGesture { showJoinGroupDialog = true }
|
||||
.confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) {
|
||||
Button(interactiveIncognito ? "Join incognito" : "Join group") {
|
||||
if unsafeToJoinIncognito(groupInfo.hostConnCustomUserProfileId) {
|
||||
AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(chat.chatInfo.apiId))
|
||||
} else {
|
||||
joinGroup(groupInfo.groupId)
|
||||
}
|
||||
Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") {
|
||||
joinGroup(groupInfo.groupId)
|
||||
}
|
||||
Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } }
|
||||
}
|
||||
@@ -101,7 +91,6 @@ struct ChatListNavLink: View {
|
||||
NavLinkPlain(
|
||||
tag: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
destination: { chatView() },
|
||||
label: { ChatPreviewView(chat: chat) },
|
||||
disabled: !groupInfo.ready
|
||||
)
|
||||
@@ -130,25 +119,13 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var interactiveIncognito: Bool {
|
||||
chat.chatInfo.incognito || chatModel.incognito
|
||||
}
|
||||
|
||||
private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View {
|
||||
Button {
|
||||
if unsafeToJoinIncognito(hostConnCustomUserProfileId) {
|
||||
AlertManager.shared.showAlert(unsafeToJoinIncognitoAlert(chat.chatInfo.apiId))
|
||||
} else {
|
||||
joinGroup(chat.chatInfo.apiId)
|
||||
}
|
||||
joinGroup(chat.chatInfo.apiId)
|
||||
} label: {
|
||||
Label("Join", systemImage: interactiveIncognito ? "theatermasks" : "ipad.and.arrow.forward")
|
||||
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
|
||||
}
|
||||
.tint(interactiveIncognito ? .indigo : .accentColor)
|
||||
}
|
||||
|
||||
private func unsafeToJoinIncognito(_ hostConnCustomUserProfileId: Int64?) -> Bool {
|
||||
interactiveIncognito && hostConnCustomUserProfileId == nil
|
||||
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
}
|
||||
|
||||
private func markReadButton() -> some View {
|
||||
@@ -344,17 +321,6 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
func unsafeToJoinIncognitoAlert(_ groupId: Int64) -> Alert {
|
||||
Alert(
|
||||
title: Text("Your main profile may be shared"),
|
||||
message: Text("The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members."),
|
||||
primaryButton: .destructive(Text("Join anyway")) {
|
||||
joinGroup(groupId)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
func joinGroup(_ groupId: Int64) {
|
||||
Task {
|
||||
logger.debug("joinGroup")
|
||||
|
||||
@@ -14,6 +14,7 @@ struct ChatListView: View {
|
||||
// not really used in this view
|
||||
@State private var showSettings = false
|
||||
@State private var searchText = ""
|
||||
@State private var selectedChat: ChatId?
|
||||
|
||||
var body: some View {
|
||||
let v = NavigationView {
|
||||
@@ -25,6 +26,7 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
selectedChat = chatModel.chatId
|
||||
if chatModel.chatId == nil, let chatId = chatModel.chatToTop {
|
||||
chatModel.chatToTop = nil
|
||||
chatModel.popChat(chatId)
|
||||
@@ -63,6 +65,15 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(
|
||||
NavigationLink(
|
||||
destination: chatView(selectedChat),
|
||||
isActive: Binding(
|
||||
get: { selectedChat != nil },
|
||||
set: { _, _ in selectedChat = nil }
|
||||
)
|
||||
) { EmptyView() }
|
||||
)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
@@ -73,6 +84,14 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatView(_ chatId: ChatId?) -> some View {
|
||||
if let chatId = chatId, let chat = chatModel.getChat(chatId) {
|
||||
ChatView(chat: chat).onAppear {
|
||||
loadChat(chat: chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func filteredChats() -> [Chat] {
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
return s == ""
|
||||
|
||||
@@ -89,7 +89,7 @@ struct ChatPreviewView: View {
|
||||
case .group(groupInfo: let groupInfo):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited:
|
||||
interactiveIncognito ? v.foregroundColor(.indigo) : v.foregroundColor(.accentColor)
|
||||
chat.chatInfo.incognito ? v.foregroundColor(.indigo) : v.foregroundColor(.accentColor)
|
||||
case .memAccepted:
|
||||
v.foregroundColor(.secondary)
|
||||
default: v
|
||||
@@ -98,10 +98,6 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var interactiveIncognito: Bool {
|
||||
chat.chatInfo.incognito || chatModel.incognito
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?, _ unread: Int) -> some View {
|
||||
if let cItem = cItem {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
@@ -131,7 +127,7 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited: chatPreviewInfoText(groupInfo.membership.memberIncognito ? "you are invited to group incognito" : "you are invited to group")
|
||||
case .memInvited: groupInvitationPreviewText(groupInfo)
|
||||
case .memAccepted: chatPreviewInfoText("connecting…")
|
||||
default: EmptyView()
|
||||
}
|
||||
@@ -140,6 +136,15 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View {
|
||||
groupInfo.membership.memberIncognito
|
||||
? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)")
|
||||
: (chatModel.incognito
|
||||
? chatPreviewInfoText("join as \(chatModel.currentUser?.profile.displayName ?? "yourself")")
|
||||
: chatPreviewInfoText("you are invited to group")
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
|
||||
Text(text)
|
||||
.frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NavLinkPlain<V: Hashable, Destination: View, Label: View>: View {
|
||||
struct NavLinkPlain<V: Hashable, Label: View>: View {
|
||||
@State var tag: V
|
||||
@Binding var selection: V?
|
||||
@ViewBuilder var destination: () -> Destination
|
||||
@ViewBuilder var label: () -> Label
|
||||
var disabled = false
|
||||
|
||||
@@ -21,10 +20,6 @@ struct NavLinkPlain<V: Hashable, Destination: View, Label: View>: View {
|
||||
.disabled(disabled)
|
||||
label()
|
||||
}
|
||||
.background {
|
||||
NavigationLink("", tag: tag, selection: $selection, destination: destination)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,12 @@ struct AddGroupView: View {
|
||||
|
||||
var body: some View {
|
||||
if let chat = chat, let groupInfo = groupInfo {
|
||||
AddGroupMembersView(chat: chat,
|
||||
groupInfo: groupInfo,
|
||||
showSkip: true) { _ in
|
||||
AddGroupMembersView(
|
||||
chat: chat,
|
||||
groupInfo: groupInfo,
|
||||
showSkip: true,
|
||||
showFooterCounter: false
|
||||
) { _ in
|
||||
dismiss()
|
||||
DispatchQueue.main.async {
|
||||
m.chatId = groupInfo.id
|
||||
@@ -46,9 +49,9 @@ struct AddGroupView: View {
|
||||
.padding(.bottom, 4)
|
||||
if (m.incognito) {
|
||||
HStack {
|
||||
Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote)
|
||||
Image(systemName: "info.circle").foregroundColor(.orange).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("You will use a random profile for this group").font(.footnote)
|
||||
Text("Incognito mode is not supported here - your main profile will be sent to group members").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
} else {
|
||||
|
||||
@@ -15,34 +15,15 @@ struct IncognitoHelp: View {
|
||||
.font(.largeTitle)
|
||||
.padding(.vertical)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
Text("Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created.")
|
||||
VStack(alignment: .leading) {
|
||||
Group {
|
||||
Text("Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.")
|
||||
Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
|
||||
Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
|
||||
Text("To find the profile used for an incognito connection, tap the contact or group name on top of the chat.")
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Incognito groups")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
Text("When you join a group incognito, your new member profile is created and shared with all members.")
|
||||
Group {
|
||||
Text("Your are incognito in a group when:")
|
||||
.padding(.top)
|
||||
textListItem("•", "the group is created in Incognito mode,")
|
||||
textListItem("•", "you or the member who invited you joined in Incognito mode,")
|
||||
textListItem("•", "you have an incognito connection with the member who invited you.")
|
||||
}
|
||||
Group {
|
||||
Text("Risks and limitations:")
|
||||
.padding(.top)
|
||||
textListItem("•", "It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile.")
|
||||
textListItem("•", "There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown.")
|
||||
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -78,6 +78,7 @@ struct SettingsView: View {
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
incognitoRow()
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
UserAddress()
|
||||
|
||||
@@ -293,6 +293,11 @@
|
||||
<target>Can't invite contact!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contacts!" xml:space="preserve">
|
||||
<source>Can't invite contacts!</source>
|
||||
<target>Can't invite contacts!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<target>Cancel</target>
|
||||
@@ -1008,19 +1013,19 @@
|
||||
<target>Incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito groups" xml:space="preserve">
|
||||
<source>Incognito groups</source>
|
||||
<target>Incognito groups</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito mode" xml:space="preserve">
|
||||
<source>Incognito mode</source>
|
||||
<target>Incognito mode</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created." xml:space="preserve">
|
||||
<source>Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created.</source>
|
||||
<target>Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created.</target>
|
||||
<trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve">
|
||||
<source>Incognito mode is not supported here - your main profile will be sent to group members</source>
|
||||
<target>Incognito mode is not supported here - your main profile will be sent to group members</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve">
|
||||
<source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source>
|
||||
<target>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incoming audio call" xml:space="preserve">
|
||||
@@ -1058,11 +1063,6 @@
|
||||
<target>Invitation expired!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invite anyway" xml:space="preserve">
|
||||
<source>Invite anyway</source>
|
||||
<target>Invite anyway</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invite members" xml:space="preserve">
|
||||
<source>Invite members</source>
|
||||
<target>Invite members</target>
|
||||
@@ -1093,11 +1093,6 @@ Please connect to the developers via Settings to receive the updates about the s
|
||||
We will be adding server redundancy to prevent lost messages.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile." xml:space="preserve">
|
||||
<source>It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile.</source>
|
||||
<target>It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve">
|
||||
<source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source>
|
||||
<target>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</target>
|
||||
@@ -1113,11 +1108,6 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Join</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Join anyway" xml:space="preserve">
|
||||
<source>Join anyway</source>
|
||||
<target>Join anyway</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Join group" xml:space="preserve">
|
||||
<source>Join group</source>
|
||||
<target>Join group</target>
|
||||
@@ -1133,11 +1123,6 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Joining group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Known main profile" xml:space="preserve">
|
||||
<source>Known main profile</source>
|
||||
<target>Known main profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Large file!" xml:space="preserve">
|
||||
<source>Large file!</source>
|
||||
<target>Large file!</target>
|
||||
@@ -1558,11 +1543,6 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Revert</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Risks and limitations:" xml:space="preserve">
|
||||
<source>Risks and limitations:</source>
|
||||
<target>Risks and limitations:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Run chat" xml:space="preserve">
|
||||
<source>Run chat</source>
|
||||
<target>Run chat</target>
|
||||
@@ -1618,6 +1598,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Search</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message" xml:space="preserve">
|
||||
<source>Send direct message</source>
|
||||
<target>Send direct message</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
<source>Send link previews</source>
|
||||
<target>Send link previews</target>
|
||||
@@ -1708,11 +1693,6 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Skipped messages</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." xml:space="preserve">
|
||||
<source>Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members.</source>
|
||||
<target>Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Somebody" xml:space="preserve">
|
||||
<source>Somebody</source>
|
||||
<target>Somebody</target>
|
||||
@@ -1808,11 +1788,6 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>The connection you accepted will be cancelled!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." xml:space="preserve">
|
||||
<source>The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members.</source>
|
||||
<target>The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve">
|
||||
<source>The contact you shared this link with will NOT be able to connect!</source>
|
||||
<target>The contact you shared this link with will NOT be able to connect!</target>
|
||||
@@ -1853,11 +1828,6 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>The sender will NOT be notified</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown." xml:space="preserve">
|
||||
<source>There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown.</source>
|
||||
<target>There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve">
|
||||
<source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source>
|
||||
<target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target>
|
||||
@@ -2037,9 +2007,9 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>When available</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When you join a group incognito, your new member profile is created and shared with all members." xml:space="preserve">
|
||||
<source>When you join a group incognito, your new member profile is created and shared with all members.</source>
|
||||
<target>When you join a group incognito, your new member profile is created and shared with all members.</target>
|
||||
<trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve">
|
||||
<source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source>
|
||||
<target>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You" xml:space="preserve">
|
||||
@@ -2137,11 +2107,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You sent group invitation</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You sent group invitation incognito" xml:space="preserve">
|
||||
<source>You sent group invitation incognito</source>
|
||||
<target>You sent group invitation incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>You will be connected when your connection request is accepted, please wait or check later!</target>
|
||||
@@ -2162,16 +2127,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You will stop receiving messages from this group. Chat history will be preserved.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will use a random profile for this group" xml:space="preserve">
|
||||
<source>You will use a random profile for this group</source>
|
||||
<target>You will use a random profile for this group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve">
|
||||
<source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source>
|
||||
<target>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve">
|
||||
<source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source>
|
||||
<target>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SMP servers" xml:space="preserve">
|
||||
<source>Your SMP servers</source>
|
||||
<target>Your SMP servers</target>
|
||||
@@ -2182,11 +2147,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Your SimpleX contact address</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your are incognito in a group when:" xml:space="preserve">
|
||||
<source>Your are incognito in a group when:</source>
|
||||
<target>Your are incognito in a group when:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your calls" xml:space="preserve">
|
||||
<source>Your calls</source>
|
||||
<target>Your calls</target>
|
||||
@@ -2244,11 +2204,6 @@ You can cancel this connection and remove the contact (and try later with a new
|
||||
<target>Your current chat database will be DELETED and REPLACED with the imported one.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your main profile may be shared" xml:space="preserve">
|
||||
<source>Your main profile may be shared</source>
|
||||
<target>Your main profile may be shared</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your privacy" xml:space="preserve">
|
||||
<source>Your privacy</source>
|
||||
<target>Your privacy</target>
|
||||
@@ -2476,11 +2431,6 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>group profile updated</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito invitation to group %@" xml:space="preserve">
|
||||
<source>incognito invitation to group %@</source>
|
||||
<target>incognito invitation to group %@</target>
|
||||
<note>group name</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>incognito via contact address link</target>
|
||||
@@ -2521,10 +2471,10 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>italic</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="known to you as %@ connected incognito" xml:space="preserve">
|
||||
<source>known to you as %@ connected incognito</source>
|
||||
<target>known to you as %@ connected incognito</target>
|
||||
<note>rcv group event chat item</note>
|
||||
<trans-unit id="join as %@" xml:space="preserve">
|
||||
<source>join as %@</source>
|
||||
<target>join as %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="left" xml:space="preserve">
|
||||
<source>left</source>
|
||||
@@ -2626,11 +2576,6 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>strike</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="the group is created in Incognito mode," xml:space="preserve">
|
||||
<source>the group is created in Incognito mode,</source>
|
||||
<target>the group is created in Incognito mode,</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="this contact" xml:space="preserve">
|
||||
<source>this contact</source>
|
||||
<target>this contact</target>
|
||||
@@ -2691,26 +2636,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>you are invited to group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you are invited to group incognito" xml:space="preserve">
|
||||
<source>you are invited to group incognito</source>
|
||||
<target>you are invited to group incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you have an incognito connection with the member who invited you." xml:space="preserve">
|
||||
<source>you have an incognito connection with the member who invited you.</source>
|
||||
<target>you have an incognito connection with the member who invited you.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you left" xml:space="preserve">
|
||||
<source>you left</source>
|
||||
<target>you left</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you or the member who invited you joined in Incognito mode," xml:space="preserve">
|
||||
<source>you or the member who invited you joined in Incognito mode,</source>
|
||||
<target>you or the member who invited you joined in Incognito mode,</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you removed %@" xml:space="preserve">
|
||||
<source>you removed %@</source>
|
||||
<target>you removed %@</target>
|
||||
|
||||
@@ -293,6 +293,11 @@
|
||||
<target>Нельзя пригласить контакт!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contacts!" xml:space="preserve">
|
||||
<source>Can't invite contacts!</source>
|
||||
<target>Нельзя пригласить контакты!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<target>Отменить</target>
|
||||
@@ -1008,19 +1013,19 @@
|
||||
<target>Инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito groups" xml:space="preserve">
|
||||
<source>Incognito groups</source>
|
||||
<target>Инкогнито группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito mode" xml:space="preserve">
|
||||
<source>Incognito mode</source>
|
||||
<target>Режим Инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created." xml:space="preserve">
|
||||
<source>Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created.</source>
|
||||
<target>Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта и группы создается новый случайный профиль.</target>
|
||||
<trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve">
|
||||
<source>Incognito mode is not supported here - your main profile will be sent to group members</source>
|
||||
<target>Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve">
|
||||
<source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source>
|
||||
<target>Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incoming audio call" xml:space="preserve">
|
||||
@@ -1058,11 +1063,6 @@
|
||||
<target>Приглашение истекло!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invite anyway" xml:space="preserve">
|
||||
<source>Invite anyway</source>
|
||||
<target>Пригласить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invite members" xml:space="preserve">
|
||||
<source>Invite members</source>
|
||||
<target>Пригласить членов группы</target>
|
||||
@@ -1093,11 +1093,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
Мы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile." xml:space="preserve">
|
||||
<source>It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile.</source>
|
||||
<target>Нельзя приглашать контакты, с которыми у вас установлено инкогнито соединение, в группу, где вы используете свой основной профиль — иначе они могут узнать ваш основной профиль.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve">
|
||||
<source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source>
|
||||
<target>Возможно, вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@).</target>
|
||||
@@ -1113,11 +1108,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Вступить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Join anyway" xml:space="preserve">
|
||||
<source>Join anyway</source>
|
||||
<target>Вступить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Join group" xml:space="preserve">
|
||||
<source>Join group</source>
|
||||
<target>Вступить в группу</target>
|
||||
@@ -1133,11 +1123,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Вступление в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Known main profile" xml:space="preserve">
|
||||
<source>Known main profile</source>
|
||||
<target>Известный основной профиль</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Large file!" xml:space="preserve">
|
||||
<source>Large file!</source>
|
||||
<target>Большой файл!</target>
|
||||
@@ -1558,11 +1543,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Отменить изменения</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Risks and limitations:" xml:space="preserve">
|
||||
<source>Risks and limitations:</source>
|
||||
<target>Риски и ограничения:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Run chat" xml:space="preserve">
|
||||
<source>Run chat</source>
|
||||
<target>Запустить chat</target>
|
||||
@@ -1618,6 +1598,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Поиск</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send direct message" xml:space="preserve">
|
||||
<source>Send direct message</source>
|
||||
<target>Отправить сообщение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
<source>Send link previews</source>
|
||||
<target>Отправлять картинки ссылок</target>
|
||||
@@ -1708,11 +1693,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Пропущенные сообщения</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." xml:space="preserve">
|
||||
<source>Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members.</source>
|
||||
<target>У некоторых выбранных контактов есть ваш основной профиль. Если они используют приложение SimpleX версии раньше чем 3.2 или другой клиент, они могут поделиться с другими участниками вашим основным профилем вместо случайного инкогнито профиля.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Somebody" xml:space="preserve">
|
||||
<source>Somebody</source>
|
||||
<target>Контакт</target>
|
||||
@@ -1808,11 +1788,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Подтвержденное соединение будет отменено!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." xml:space="preserve">
|
||||
<source>The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members.</source>
|
||||
<target>У контакта есть ваш основной профиль. Если он использует приложение SimpleX версии раньше чем 3.2 или другой клиент, он может поделиться с другими членами группы вашим основным профилем вместо случайного инкогнито профиля.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve">
|
||||
<source>The contact you shared this link with will NOT be able to connect!</source>
|
||||
<target>Контакт, которому вы отправили эту ссылку, не сможет соединиться!</target>
|
||||
@@ -1853,11 +1828,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Отправитель не будет уведомлён</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown." xml:space="preserve">
|
||||
<source>There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown.</source>
|
||||
<target>Если в инкогнито группе есть члены, которые знают ваш основной профиль, существует риск его раскрытия. Перед тем, как вы пригласите ваши контакты или вступите в такую группу, будет показано предупреждение.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve">
|
||||
<source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source>
|
||||
<target>Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.</target>
|
||||
@@ -2037,9 +2007,9 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Когда возможно</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When you join a group incognito, your new member profile is created and shared with all members." xml:space="preserve">
|
||||
<source>When you join a group incognito, your new member profile is created and shared with all members.</source>
|
||||
<target>Когда вы вступаете в группу инкогнито, создается новый случайный профиль, который отправляется другим членам группы.</target>
|
||||
<trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve">
|
||||
<source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source>
|
||||
<target>Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You" xml:space="preserve">
|
||||
@@ -2137,11 +2107,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Вы отправили приглашение в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You sent group invitation incognito" xml:space="preserve">
|
||||
<source>You sent group invitation incognito</source>
|
||||
<target>Вы отправили приглашение в группу инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</target>
|
||||
@@ -2162,16 +2127,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Вы перестанете получать сообщения от этой группы. История чата будет сохранена.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will use a random profile for this group" xml:space="preserve">
|
||||
<source>You will use a random profile for this group</source>
|
||||
<target>Вы будете использовать случайный профиль для этой группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve">
|
||||
<source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source>
|
||||
<target>Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve">
|
||||
<source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source>
|
||||
<target>Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your SMP servers" xml:space="preserve">
|
||||
<source>Your SMP servers</source>
|
||||
<target>Ваши SMP серверы</target>
|
||||
@@ -2182,11 +2147,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Ваш SimpleX адрес</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your are incognito in a group when:" xml:space="preserve">
|
||||
<source>Your are incognito in a group when:</source>
|
||||
<target>Вы инкогнито в группе, когда:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your calls" xml:space="preserve">
|
||||
<source>Your calls</source>
|
||||
<target>Ваши звонки</target>
|
||||
@@ -2209,7 +2169,7 @@ To connect, please ask your contact to create another connection link and check
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat profile will be sent to group members" xml:space="preserve">
|
||||
<source>Your chat profile will be sent to group members</source>
|
||||
<target>Ваш профиль чата будет отправлен участникам группы</target>
|
||||
<target>Ваш профиль чата будет отправлен членам группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve">
|
||||
@@ -2244,11 +2204,6 @@ You can cancel this connection and remove the contact (and try later with a new
|
||||
<target>Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your main profile may be shared" xml:space="preserve">
|
||||
<source>Your main profile may be shared</source>
|
||||
<target>Вашим основным профилем могут поделиться</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your privacy" xml:space="preserve">
|
||||
<source>Your privacy</source>
|
||||
<target>Конфиденциальность</target>
|
||||
@@ -2476,11 +2431,6 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>профиль группы обновлен</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito invitation to group %@" xml:space="preserve">
|
||||
<source>incognito invitation to group %@</source>
|
||||
<target>инкогнито приглашение в группу %@</target>
|
||||
<note>group name</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>инкогнито через ссылку-контакт</target>
|
||||
@@ -2521,10 +2471,10 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>курсив</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="known to you as %@ connected incognito" xml:space="preserve">
|
||||
<source>known to you as %@ connected incognito</source>
|
||||
<target>известный(ая) вам как %@ соединен(а) инкогнито</target>
|
||||
<note>rcv group event chat item</note>
|
||||
<trans-unit id="join as %@" xml:space="preserve">
|
||||
<source>join as %@</source>
|
||||
<target>вступить как %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="left" xml:space="preserve">
|
||||
<source>left</source>
|
||||
@@ -2626,11 +2576,6 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>зачеркнуть</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="the group is created in Incognito mode," xml:space="preserve">
|
||||
<source>the group is created in Incognito mode,</source>
|
||||
<target>группа создана в режиме Инкогнито,</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="this contact" xml:space="preserve">
|
||||
<source>this contact</source>
|
||||
<target>этот контакт</target>
|
||||
@@ -2691,26 +2636,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>вы приглашены в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you are invited to group incognito" xml:space="preserve">
|
||||
<source>you are invited to group incognito</source>
|
||||
<target>вы приглашены в группу инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you have an incognito connection with the member who invited you." xml:space="preserve">
|
||||
<source>you have an incognito connection with the member who invited you.</source>
|
||||
<target>вы соединены инкогнито с членом группы, который вас пригласил.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you left" xml:space="preserve">
|
||||
<source>you left</source>
|
||||
<target>вы покинули группу</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you or the member who invited you joined in Incognito mode," xml:space="preserve">
|
||||
<source>you or the member who invited you joined in Incognito mode,</source>
|
||||
<target>вы или пригласивший вас член группы вступили в группу в режиме Инкогнито,</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you removed %@" xml:space="preserve">
|
||||
<source>you removed %@</source>
|
||||
<target>вы удалили %@</target>
|
||||
|
||||
@@ -1164,7 +1164,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1206,7 +1206,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1285,7 +1285,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1315,7 +1315,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1346,7 +1346,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -1392,7 +1392,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
||||
@@ -232,7 +232,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case userSMPServers(smpServers: [String])
|
||||
case networkConfig(networkConfig: NetCfg)
|
||||
case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
|
||||
case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?, localMainProfile: LocalProfile?)
|
||||
case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
|
||||
case invitation(connReqInvitation: String)
|
||||
case sentConfirmation
|
||||
case sentInvitation
|
||||
@@ -416,7 +416,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .userSMPServers(smpServers): return String(describing: smpServers)
|
||||
case let .networkConfig(networkConfig): return String(describing: networkConfig)
|
||||
case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))"
|
||||
case let .groupMemberInfo(groupInfo, member, connectionStats_, localMainProfile): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))\nlocalMainProfile: \(String(describing: localMainProfile))"
|
||||
case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))"
|
||||
case let .invitation(connReqInvitation): return connReqInvitation
|
||||
case .sentConfirmation: return noDetails
|
||||
case .sentInvitation: return noDetails
|
||||
|
||||
@@ -1390,12 +1390,9 @@ public struct CIGroupInvitation: Decodable {
|
||||
public var localDisplayName: GroupName
|
||||
public var groupProfile: GroupProfile
|
||||
public var status: CIGroupInvitationStatus
|
||||
public var invitedIncognito: Bool?
|
||||
|
||||
var text: String {
|
||||
(invitedIncognito ?? false) ?
|
||||
String.localizedStringWithFormat(NSLocalizedString("incognito invitation to group %@", comment: "group name"), groupProfile.displayName)
|
||||
: String.localizedStringWithFormat(NSLocalizedString("invitation to group %@", comment: "group name"), groupProfile.displayName)
|
||||
String.localizedStringWithFormat(NSLocalizedString("invitation to group %@", comment: "group name"), groupProfile.displayName)
|
||||
}
|
||||
|
||||
public static func getSample(groupId: Int64 = 1, groupMemberId: Int64 = 1, localDisplayName: GroupName = "team", groupProfile: GroupProfile = GroupProfile.sampleData, status: CIGroupInvitationStatus = .pending) -> CIGroupInvitation {
|
||||
@@ -1412,7 +1409,7 @@ public enum CIGroupInvitationStatus: String, Decodable {
|
||||
|
||||
public enum RcvGroupEvent: Decodable {
|
||||
case memberAdded(groupMemberId: Int64, profile: Profile)
|
||||
case memberConnected(contactMainProfile: Profile?)
|
||||
case memberConnected
|
||||
case memberLeft
|
||||
case memberDeleted(groupMemberId: Int64, profile: Profile)
|
||||
case userDeleted
|
||||
@@ -1423,12 +1420,7 @@ public enum RcvGroupEvent: Decodable {
|
||||
switch self {
|
||||
case let .memberAdded(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName)
|
||||
case let .memberConnected(contactMainProfile):
|
||||
if let contactMainProfile = contactMainProfile {
|
||||
return String.localizedStringWithFormat(NSLocalizedString("known to you as %@ connected incognito", comment: "rcv group event chat item"), contactMainProfile.profileViewName)
|
||||
} else {
|
||||
return NSLocalizedString("member connected", comment: "rcv group event chat item")
|
||||
}
|
||||
case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item")
|
||||
case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item")
|
||||
case let .memberDeleted(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName)
|
||||
|
||||
@@ -212,6 +212,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contact!" = "Нельзя пригласить контакт!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contacts!" = "Нельзя пригласить контакты!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "Отменить";
|
||||
|
||||
@@ -719,17 +722,14 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Incognito" = "Инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incognito groups" = "Инкогнито группы";
|
||||
|
||||
/* group name */
|
||||
"incognito invitation to group %@" = "инкогнито приглашение в группу %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incognito mode" = "Режим Инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incognito mode protects the privacy of your main profile name and image — for each new contact and group a new random profile is created." = "Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта и группы создается новый случайный профиль.";
|
||||
"Incognito mode is not supported here - your main profile will be sent to group members" = "Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль.";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via contact address link" = "инкогнито через ссылку-контакт";
|
||||
@@ -764,9 +764,6 @@
|
||||
/* group name */
|
||||
"invitation to group %@" = "приглашение в группу %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Invite anyway" = "Пригласить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Invite members" = "Пригласить членов группы";
|
||||
|
||||
@@ -788,9 +785,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"It can happen when:\n1. The messages expire on the server if they were not received for 30 days,\n2. The server you use to receive the messages from this contact was updated and restarted.\n3. The connection is compromised.\nPlease connect to the developers via Settings to receive the updates about the servers.\nWe will be adding server redundancy to prevent lost messages." = "Это может случится, когда:\n1. Сервер удалил сообщения, если они не были доставлены в течение 30 дней.\n2. Сервер, через который вы получаете сообщения от контакта, был обновлён и перезапущен.\n3. Соединение компроментировано.\nПожалуйста, соединитесь с девелоперами через Настройки, чтобы получать уведомления о серверах.\nМы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"It is not allowed to invite contacts with whom you have an incognito connection to a group where you use your main profile – otherwise they might find out your main profile." = "Нельзя приглашать контакты, с которыми у вас установлено инкогнито соединение, в группу, где вы используете свой основной профиль — иначе они могут узнать ваш основной профиль.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Возможно, вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@).";
|
||||
|
||||
@@ -804,7 +798,7 @@
|
||||
"Join" = "Вступить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Join anyway" = "Вступить";
|
||||
"join as %@" = "вступить как %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Join group" = "Вступить в группу";
|
||||
@@ -815,12 +809,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Joining group" = "Вступление в группу";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Known main profile" = "Известный основной профиль";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"known to you as %@ connected incognito" = "известный(ая) вам как %@ соединен(а) инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Large file!" = "Большой файл!";
|
||||
|
||||
@@ -1121,9 +1109,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Revert" = "Отменить изменения";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Risks and limitations:" = "Риски и ограничения:";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Run chat" = "Запустить chat";
|
||||
|
||||
@@ -1157,6 +1142,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"secret" = "секрет";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send direct message" = "Отправить сообщение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send link previews" = "Отправлять картинки ссылок";
|
||||
|
||||
@@ -1217,9 +1205,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"SMP servers (one per line)" = "SMP серверы (один на строке)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Some selected contacts have your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." = "У некоторых выбранных контактов есть ваш основной профиль. Если они используют приложение SimpleX версии раньше чем 3.2 или другой клиент, они могут поделиться с другими участниками вашим основным профилем вместо случайного инкогнито профиля.";
|
||||
|
||||
/* notification title */
|
||||
"Somebody" = "Контакт";
|
||||
|
||||
@@ -1283,18 +1268,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"The connection you accepted will be cancelled!" = "Подтвержденное соединение будет отменено!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The contact who invited you has your main profile. If they use SimpleX app older than v3.2 or some other client, they may share your main profile instead of a random incognito profile with other members." = "У контакта есть ваш основной профиль. Если он использует приложение SimpleX версии раньше чем 3.2 или другой клиент, он может поделиться с другими членами группы вашим основным профилем вместо случайного инкогнито профиля.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The contact you shared this link with will NOT be able to connect!" = "Контакт, которому вы отправили эту ссылку, не сможет соединиться!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The created archive is available via app Settings / Database / Old database archive." = "Созданный архив доступен через Настройки приложения.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"the group is created in Incognito mode," = "группа создана в режиме Инкогнито,";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"The group is fully decentralized – it is visible only to the members." = "Группа полностью децентрализована — она видна только членам.";
|
||||
|
||||
@@ -1313,9 +1292,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"The sender will NOT be notified" = "Отправитель не будет уведомлён";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"There is a risk to have your main profile shared, if you have contacts who know your main profile in an incognito group. Before you invite or join group with such contacts a warning will be shown." = "Если в инкогнито группе есть члены, которые знают ваш основной профиль, существует риск его раскрытия. Перед тем, как вы пригласите ваши контакты или вступите в такую группу, будет показано предупреждение.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.";
|
||||
|
||||
@@ -1455,7 +1431,7 @@
|
||||
"When available" = "Когда возможно";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"When you join a group incognito, your new member profile is created and shared with all members." = "Когда вы вступаете в группу инкогнито, создается новый случайный профиль, который отправляется другим членам группы.";
|
||||
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You" = "Вы";
|
||||
@@ -1475,9 +1451,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You are invited to group" = "Вы приглашены в группу";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"you are invited to group incognito" = "вы приглашены в группу инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**.";
|
||||
|
||||
@@ -1502,9 +1475,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"you have an incognito connection with the member who invited you." = "вы соединены инкогнито с членом группы, который вас пригласил.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You invited your contact" = "Вы пригласили ваш контакт";
|
||||
|
||||
@@ -1520,9 +1490,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, инача вы можете перестать получать сообщения от каких то контактов.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"you or the member who invited you joined in Incognito mode," = "вы или пригласивший вас член группы вступили в группу в режиме Инкогнито,";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You rejected group invitation" = "Вы отклонили приглашение в группу";
|
||||
|
||||
@@ -1532,9 +1499,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You sent group invitation" = "Вы отправили приглашение в группу";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You sent group invitation incognito" = "Вы отправили приглашение в группу инкогнито";
|
||||
|
||||
/* chat list item description */
|
||||
"you shared one-time link" = "вы создали ссылку";
|
||||
|
||||
@@ -1553,9 +1517,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will use a random profile for this group" = "Вы будете использовать случайный профиль для этой группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"you: " = "вы: ";
|
||||
|
||||
@@ -1563,7 +1524,7 @@
|
||||
"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your are incognito in a group when:" = "Вы инкогнито в группе, когда:";
|
||||
"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your calls" = "Ваши звонки";
|
||||
@@ -1578,7 +1539,7 @@
|
||||
"Your chat profile" = "Ваш профиль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat profile will be sent to group members" = "Ваш профиль чата будет отправлен участникам группы";
|
||||
"Your chat profile will be sent to group members" = "Ваш профиль чата будет отправлен членам группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat profile will be sent to your contact" = "Ваш профиль будет отправлен вашему контакту";
|
||||
@@ -1598,9 +1559,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Your current chat database will be DELETED and REPLACED with the imported one." = "Текущие данные вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your main profile may be shared" = "Вашим основным профилем могут поделиться";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your privacy" = "Конфиденциальность";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user