mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74b11d1c5d | |||
| a1562bf0e7 | |||
| 73447ce22b | |||
| 956cf6b203 | |||
| 378118b82e | |||
| 92abdde69e | |||
| 69758971af | |||
| 5fa2d7f7ce | |||
| 0169589e7c | |||
| 2d4348c50d | |||
| 14f6e5c16f | |||
| 51a2fa8c28 | |||
| 7343e4a51a | |||
| b4d7afb4c1 | |||
| 2fc6873c42 | |||
| 7683254de2 | |||
| 3a077d927d | |||
| a5e74ea2f0 | |||
| 53a71cf28c | |||
| e6551abc68 | |||
| bd7aa81625 | |||
| 04592f52de | |||
| a06499d710 | |||
| 6d1414af71 | |||
| 9cd7a7fdb0 | |||
| 3c2f5d14f5 | |||
| 985f3dffd3 | |||
| 0a048eb286 | |||
| 2cffe91e0b |
@@ -11,7 +11,7 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 49
|
||||
versionCode 51
|
||||
versionName "3.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -94,6 +94,7 @@ dependencies {
|
||||
implementation "androidx.navigation:navigation-compose:2.4.1"
|
||||
implementation "com.google.accompanist:accompanist-insets:0.23.0"
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation "com.godaddy.android.colorpicker:compose-color-picker:0.4.2"
|
||||
|
||||
def work_version = "2.7.1"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
@@ -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)
|
||||
@@ -66,6 +67,7 @@ class ChatModel(val controller: ChatController) {
|
||||
|
||||
fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null
|
||||
fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id }
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
|
||||
@@ -290,19 +292,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 +317,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 {
|
||||
@@ -325,6 +329,7 @@ interface SomeChat {
|
||||
val apiId: Long
|
||||
val ready: Boolean
|
||||
val sendMsgEnabled: Boolean
|
||||
val ntfsEnabled: Boolean
|
||||
val createdAt: Instant
|
||||
val updatedAt: Instant
|
||||
}
|
||||
@@ -375,6 +380,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
|
||||
@@ -383,11 +390,14 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val apiId get() = contact.apiId
|
||||
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)
|
||||
@@ -402,11 +412,14 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val apiId get() = groupInfo.apiId
|
||||
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)
|
||||
@@ -421,11 +434,14 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val apiId get() = contactRequest.apiId
|
||||
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)
|
||||
@@ -440,11 +456,14 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val apiId get() = contactConnection.apiId
|
||||
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 =
|
||||
@@ -454,13 +473,13 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Contact(
|
||||
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,
|
||||
val chatSettings: ChatSettings,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -469,19 +488,25 @@ class Contact(
|
||||
override val apiId get() = contactId
|
||||
override val ready get() = activeConn.connStatus == ConnStatus.Ready
|
||||
override val sendMsgEnabled get() = true
|
||||
override val displayName get() = profile.displayName
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||
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(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
@@ -503,10 +528,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,13 +539,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",
|
||||
@@ -529,6 +557,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,
|
||||
@@ -536,12 +586,13 @@ class Group (
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class GroupInfo (
|
||||
data class GroupInfo (
|
||||
val groupId: Long,
|
||||
override val localDisplayName: String,
|
||||
val groupProfile: GroupProfile,
|
||||
val membership: GroupMember,
|
||||
// val chatSettings: ChatSettings,
|
||||
val hostConnCustomUserProfileId: Long? = null,
|
||||
val chatSettings: ChatSettings,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -550,9 +601,11 @@ class GroupInfo (
|
||||
override val apiId get() = groupId
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = membership.memberActive
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||
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
|
||||
@@ -569,6 +622,8 @@ class GroupInfo (
|
||||
localDisplayName = "team",
|
||||
groupProfile = GroupProfile.sampleData,
|
||||
membership = GroupMember.sampleData,
|
||||
hostConnCustomUserProfileId = null,
|
||||
chatSettings = ChatSettings(true),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
@@ -579,7 +634,8 @@ 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(
|
||||
@@ -599,17 +655,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
|
||||
@@ -645,6 +702,8 @@ class GroupMember (
|
||||
&& userRole >= GroupMemberRole.Admin && userRole >= memberRole && membership.memberCurrent
|
||||
}
|
||||
|
||||
val memberIncognito = memberProfile.profileId != memberContactProfileId
|
||||
|
||||
companion object {
|
||||
val sampleData = GroupMember(
|
||||
groupMemberId = 1,
|
||||
@@ -655,8 +714,9 @@ class GroupMember (
|
||||
memberStatus = GroupMemberStatus.MemComplete,
|
||||
invitedBy = InvitedBy.IBUser(),
|
||||
localDisplayName = "alice",
|
||||
memberProfile = Profile.sampleData,
|
||||
memberProfile = LocalProfile.sampleData,
|
||||
memberContactId = 1,
|
||||
memberContactProfileId = 1L,
|
||||
activeConn = Connection.sampleData
|
||||
)
|
||||
}
|
||||
@@ -770,9 +830,11 @@ class UserContactRequest (
|
||||
override val apiId get() = contactRequestId
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
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(
|
||||
@@ -791,6 +853,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 {
|
||||
@@ -799,6 +862,7 @@ class PendingContactConnection(
|
||||
override val apiId get() = pccConnId
|
||||
override val ready get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId)
|
||||
override val displayName: String get() {
|
||||
val initiated = pccConnStatus.initiated
|
||||
@@ -814,14 +878,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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -832,6 +903,7 @@ class PendingContactConnection(
|
||||
pccAgentConnId = "abcd",
|
||||
pccConnStatus = status,
|
||||
viaContactUri = viaContactUri,
|
||||
customUserProfileId = null,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
@@ -886,7 +958,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() =
|
||||
@@ -1179,9 +1251,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(
|
||||
|
||||
@@ -64,6 +64,8 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
|
||||
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (!cInfo.ntfsEnabled) return
|
||||
|
||||
notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo
|
||||
import android.app.Application
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
@@ -16,12 +15,14 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.call.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
@@ -87,6 +88,8 @@ class AppPreferences(val context: Context) {
|
||||
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
|
||||
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
|
||||
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
|
||||
val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name)
|
||||
val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false)
|
||||
val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout)
|
||||
val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout)
|
||||
val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval)
|
||||
@@ -94,6 +97,10 @@ 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())
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
Preference(
|
||||
@@ -154,6 +161,8 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
|
||||
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
|
||||
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
|
||||
private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode"
|
||||
private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode"
|
||||
private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout"
|
||||
private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout"
|
||||
private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval"
|
||||
@@ -161,6 +170,9 @@ 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +185,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) {
|
||||
@@ -183,6 +196,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()
|
||||
@@ -291,6 +305,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
|
||||
@@ -388,9 +408,20 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiContactInfo(contactId: Long): ConnectionStats? {
|
||||
suspend fun apiSetSettings(type: ChatType,id: Long, settings: ChatSettings): Boolean {
|
||||
val r = sendCmd(CC.APISetChatSettings(type, id, settings))
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
else -> {
|
||||
Log.e(TAG, "apiSetSettings bad response: ${r.responseType} ${r.details}")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -501,6 +532,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
|
||||
@@ -942,7 +980,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) }
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -973,7 +1011,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = ignoreOptimization) { Text(stringResource(R.string.ok)) }
|
||||
TextButton(onClick = ignoreOptimization) { Text(stringResource(R.string.ok)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -999,7 +1037,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) }
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1069,6 +1107,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
fun getNetCfg(): NetCfg {
|
||||
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
|
||||
val socksProxy = if (useSocksProxy) ":9050" else null
|
||||
val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!)
|
||||
val requiredHostMode = appPrefs.networkRequiredHostMode.get()
|
||||
val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get()
|
||||
val tcpTimeout = appPrefs.networkTCPTimeout.get()
|
||||
val smpPingInterval = appPrefs.networkSMPPingInterval.get()
|
||||
@@ -1083,6 +1123,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
}
|
||||
return NetCfg(
|
||||
socksProxy = socksProxy,
|
||||
hostMode = hostMode,
|
||||
requiredHostMode = requiredHostMode,
|
||||
tcpConnectTimeout = tcpConnectTimeout,
|
||||
tcpTimeout = tcpTimeout,
|
||||
tcpKeepAlive = tcpKeepAlive,
|
||||
@@ -1092,6 +1134,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
|
||||
|
||||
fun setNetCfg(cfg: NetCfg) {
|
||||
appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy)
|
||||
appPrefs.networkHostMode.set(cfg.hostMode.name)
|
||||
appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode)
|
||||
appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout)
|
||||
appPrefs.networkTCPTimeout.set(cfg.tcpTimeout)
|
||||
appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval)
|
||||
@@ -1116,6 +1160,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()
|
||||
@@ -1146,6 +1191,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()
|
||||
@@ -1168,6 +1214,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"
|
||||
@@ -1197,6 +1244,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"
|
||||
@@ -1220,6 +1268,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"
|
||||
@@ -1249,6 +1298,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"
|
||||
@@ -1330,6 +1380,26 @@ data class NetCfg(
|
||||
smpPingInterval = 600_000_000
|
||||
)
|
||||
}
|
||||
|
||||
val onionHosts: OnionHosts get() = when {
|
||||
hostMode == HostMode.Public && requiredHostMode -> OnionHosts.NEVER
|
||||
hostMode == HostMode.OnionViaSocks && !requiredHostMode -> OnionHosts.PREFER
|
||||
hostMode == HostMode.OnionViaSocks && requiredHostMode -> OnionHosts.REQUIRED
|
||||
else -> OnionHosts.PREFER
|
||||
}
|
||||
|
||||
fun withOnionHosts(mode: OnionHosts): NetCfg = when (mode) {
|
||||
OnionHosts.NEVER ->
|
||||
this.copy(hostMode = HostMode.Public, requiredHostMode = true)
|
||||
OnionHosts.PREFER ->
|
||||
this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = false)
|
||||
OnionHosts.REQUIRED ->
|
||||
this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = true)
|
||||
}
|
||||
}
|
||||
|
||||
enum class OnionHosts {
|
||||
NEVER, PREFER, REQUIRED
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1395,7 +1465,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()
|
||||
@@ -1405,6 +1475,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()
|
||||
@@ -1491,6 +1562,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"
|
||||
@@ -1575,6 +1647,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)
|
||||
|
||||
@@ -2,10 +2,15 @@ package chat.simplex.app.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
enum class DefaultTheme {
|
||||
SYSTEM, DARK, LIGHT
|
||||
}
|
||||
|
||||
val DarkColorPalette = darkColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
primaryVariant = SimplexGreen,
|
||||
secondary = DarkGray,
|
||||
@@ -18,7 +23,7 @@ private val DarkColorPalette = darkColors(
|
||||
onSurface = Color(0xFFFFFBFA),
|
||||
// onError: Color = Color.Black,
|
||||
)
|
||||
private val LightColorPalette = lightColors(
|
||||
val LightColorPalette = lightColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
primaryVariant = SimplexGreen,
|
||||
secondary = LightGray,
|
||||
@@ -30,16 +35,28 @@ private val LightColorPalette = lightColors(
|
||||
// onSurface = Color.Black,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SimpleXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||
val colors = if (darkTheme) {
|
||||
DarkColorPalette
|
||||
} else {
|
||||
LightColorPalette
|
||||
}
|
||||
val CurrentColors: MutableStateFlow<Pair<Colors, DefaultTheme>> = MutableStateFlow(ThemeManager.currentColors(true))
|
||||
|
||||
@Composable
|
||||
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.first.isLight
|
||||
|
||||
@Composable
|
||||
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
LaunchedEffect(darkTheme) {
|
||||
// For preview
|
||||
if (darkTheme != null)
|
||||
CurrentColors.value = ThemeManager.currentColors(darkTheme)
|
||||
}
|
||||
val systemDark = isSystemInDarkTheme()
|
||||
LaunchedEffect(systemDark) {
|
||||
if (CurrentColors.value.second == DefaultTheme.SYSTEM && CurrentColors.value.first.isLight == systemDark) {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark)
|
||||
}
|
||||
}
|
||||
val theme by CurrentColors.collectAsState()
|
||||
MaterialTheme(
|
||||
colors = colors,
|
||||
colors = theme.first,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = content
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package chat.simplex.app.ui.theme
|
||||
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
object ThemeManager {
|
||||
private val appPrefs: AppPreferences by lazy {
|
||||
AppPreferences(SimplexApp.context)
|
||||
}
|
||||
|
||||
fun currentColors(darkForSystemTheme: Boolean): Pair<Colors, DefaultTheme> {
|
||||
val theme = appPrefs.currentTheme.get()!!
|
||||
val systemThemeColors = if (darkForSystemTheme) DarkColorPalette else LightColorPalette
|
||||
val res = when (theme) {
|
||||
DefaultTheme.SYSTEM.name -> Pair(systemThemeColors, DefaultTheme.SYSTEM)
|
||||
DefaultTheme.DARK.name -> Pair(DarkColorPalette, DefaultTheme.DARK)
|
||||
DefaultTheme.LIGHT.name -> Pair(LightColorPalette, DefaultTheme.LIGHT)
|
||||
else -> Pair(systemThemeColors, DefaultTheme.SYSTEM)
|
||||
}
|
||||
return res.copy(first = res.first.copy(primary = Color(appPrefs.primaryColor.get())))
|
||||
}
|
||||
|
||||
// colors, default theme enum, localized name of theme
|
||||
fun allThemes(darkForSystemTheme: Boolean): List<Triple<Colors, DefaultTheme, String>> {
|
||||
val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>()
|
||||
allThemes.add(
|
||||
Triple(
|
||||
if (darkForSystemTheme) DarkColorPalette else LightColorPalette,
|
||||
DefaultTheme.SYSTEM,
|
||||
generalGetString(R.string.theme_system)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
LightColorPalette,
|
||||
DefaultTheme.LIGHT,
|
||||
generalGetString(R.string.theme_light)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
DarkColorPalette,
|
||||
DefaultTheme.DARK,
|
||||
generalGetString(R.string.theme_dark)
|
||||
)
|
||||
)
|
||||
return allThemes
|
||||
}
|
||||
|
||||
fun applyTheme(name: String, darkForSystemTheme: Boolean) {
|
||||
appPrefs.currentTheme.set(name)
|
||||
CurrentColors.value = currentColors(darkForSystemTheme)
|
||||
}
|
||||
|
||||
fun saveAndApplyPrimaryColor(color: Color) {
|
||||
appPrefs.primaryColor.set(color.toArgb())
|
||||
CurrentColors.value = currentColors(!CurrentColors.value.first.isLight)
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ fun IncomingCallAlertLayout(
|
||||
ignoreCall: () -> Unit,
|
||||
acceptCall: () -> Unit
|
||||
) {
|
||||
val color = if (isSystemInDarkTheme()) IncomingCallDark else IncomingCallLight
|
||||
val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight
|
||||
Column(Modifier.background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) {
|
||||
IncomingCallInfo(invitation)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
@@ -1,47 +1,102 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import InfoRow
|
||||
import InfoRowEllipsis
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
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.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.*
|
||||
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) }
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
changeNtfsState = { enabled ->
|
||||
changeNtfsState(enabled, chat, chatModel)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeNtfsState(enabled: Boolean, chat: Chat, chatModel: ChatModel) {
|
||||
val newChatInfo = when(chat.chatInfo) {
|
||||
is ChatInfo.Direct -> with (chat.chatInfo) {
|
||||
ChatInfo.Direct(contact.copy(chatSettings = contact.chatSettings.copy(enableNtfs = enabled)))
|
||||
}
|
||||
is ChatInfo.Group -> with(chat.chatInfo) {
|
||||
ChatInfo.Group(groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(enableNtfs = enabled)))
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
withApi {
|
||||
val res = when (newChatInfo) {
|
||||
is ChatInfo.Direct -> with(newChatInfo) {
|
||||
chatModel.controller.apiSetSettings(chatType, apiId, contact.chatSettings)
|
||||
}
|
||||
is ChatInfo.Group -> with(newChatInfo) {
|
||||
chatModel.controller.apiSetSettings(chatType, apiId, groupInfo.chatSettings)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
if (res && newChatInfo != null) {
|
||||
chatModel.updateChatInfo(newChatInfo)
|
||||
if (!enabled) {
|
||||
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.delete_contact_question),
|
||||
@@ -82,10 +137,15 @@ 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
|
||||
clearChat: () -> Unit,
|
||||
changeNtfsState: (Boolean) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -97,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) {
|
||||
@@ -120,6 +190,17 @@ fun ChatInfoLayout(
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
var ntfsEnabled by remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
SectionView(title = stringResource(R.string.settings_section_title_settings)) {
|
||||
SectionItemView {
|
||||
NtfsSwitch(ntfsEnabled) {
|
||||
ntfsEnabled = !ntfsEnabled
|
||||
changeNtfsState(ntfsEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
ClearChatButton(clearChat)
|
||||
@@ -143,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 (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
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,
|
||||
@@ -166,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(
|
||||
@@ -223,7 +330,43 @@ fun ServerImage(networkStatus: Chat.NetworkStatus) {
|
||||
@Composable
|
||||
fun SimplexServers(text: String, servers: List<String>) {
|
||||
val info = servers.joinToString(separator = ", ") { it.substringAfter("@") }
|
||||
InfoRow(text, info)
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
InfoRowEllipsis(text, info) {
|
||||
clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ",")))
|
||||
Toast.makeText(SimplexApp.context, generalGetString(R.string.copied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NtfsSwitch(
|
||||
ntfsEnabled: Boolean,
|
||||
toggleNtfs: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Notifications,
|
||||
stringResource(R.string.notifications),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
Text(stringResource(R.string.notifications))
|
||||
}
|
||||
Switch(
|
||||
checked = ntfsEnabled,
|
||||
onCheckedChange = toggleNtfs,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -262,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() {
|
||||
@@ -272,8 +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 = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.call.*
|
||||
import chat.simplex.app.views.chat.group.AddGroupMembersView
|
||||
import chat.simplex.app.views.chat.group.GroupChatInfoView
|
||||
import chat.simplex.app.views.chat.group.*
|
||||
import chat.simplex.app.views.chat.item.ChatItemView
|
||||
import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.chatlist.*
|
||||
@@ -56,6 +55,22 @@ fun ChatView(chatModel: ChatModel) {
|
||||
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value.
|
||||
// With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
activeChat = if (chatModel.chatId.value == null) {
|
||||
null
|
||||
} else {
|
||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||
// Also for situation when chatId changes after clicking in notification, etc
|
||||
chatModel.getChat(chatModel.chatId.value!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeChat == null || user == null) {
|
||||
chatModel.chatId.value = null
|
||||
} else {
|
||||
@@ -89,18 +104,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 (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
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) {
|
||||
@@ -108,7 +126,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
GroupChatInfoView(chatModel, close)
|
||||
}
|
||||
@@ -116,15 +134,16 @@ fun ChatView(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
val c = chatModel.chats.firstOrNull {
|
||||
it.chatInfo is ChatInfo.Direct && it.chatInfo.contact.contactId == contactId
|
||||
}
|
||||
if (c != null) {
|
||||
withApi {
|
||||
openChat(c.chatInfo, chatModel)
|
||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||
activeChat = c
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
withApi {
|
||||
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, stats, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -177,7 +196,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
AddGroupMembersView(groupInfo, chatModel, close)
|
||||
}
|
||||
@@ -220,9 +239,10 @@ fun ChatLayout(
|
||||
chatItems: List<ChatItem>,
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
chatModelIncognito: Boolean,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -252,24 +272,22 @@ fun ChatLayout(
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) }
|
||||
val setFloatingButton = { button: @Composable () -> Unit ->
|
||||
floatingButton.value = button
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers, onSearchValueChanged) },
|
||||
bottomBar = composeView,
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = floatingButton.value,
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
) { contentPadding ->
|
||||
CompositionLocalProvider(
|
||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
BoxWithConstraints(Modifier.padding(contentPadding)) {
|
||||
ChatItemsList(
|
||||
user, chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, openDirectChat, loadPrevMessages, deleteMessage,
|
||||
receiveFile, joinGroup, acceptCall, markRead, floatingButton
|
||||
)
|
||||
}
|
||||
BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) {
|
||||
ChatItemsList(
|
||||
user, chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
receiveFile, joinGroup, acceptCall, markRead, setFloatingButton
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +339,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
|
||||
@@ -365,6 +383,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),
|
||||
@@ -374,7 +395,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
|
||||
@@ -405,14 +426,15 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
chatItems: List<ChatItem>,
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
chatModelIncognito: Boolean,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
floatingButton: MutableState<@Composable () -> Unit>
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -449,86 +471,90 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } }
|
||||
LazyColumn(state = listState, reverseLayout = true) {
|
||||
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
itemsIndexed(reversedChatItems) { i, cItem ->
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false }
|
||||
val directions = setOf(DismissDirection.EndToStart)
|
||||
val swipeableModifier = SwipeToDismissModifier(
|
||||
state = dismissState,
|
||||
directions = directions,
|
||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||
)
|
||||
val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd))
|
||||
val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart))
|
||||
if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) {
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
|
||||
val member = cItem.chatDir.groupMember
|
||||
val showMember = showMemberImage(member, prevItem)
|
||||
Row(Modifier.padding(start = 8.dp, end = 66.dp).then(swipeableModifier)) {
|
||||
if (showMember) {
|
||||
val contactId = member.memberContactId
|
||||
if (contactId == null) {
|
||||
MemberImage(member)
|
||||
CompositionLocalProvider(
|
||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false }
|
||||
val directions = setOf(DismissDirection.EndToStart)
|
||||
val swipeableModifier = SwipeToDismissModifier(
|
||||
state = dismissState,
|
||||
directions = directions,
|
||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||
)
|
||||
val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd))
|
||||
val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart))
|
||||
if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) {
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
openDirectChat(contactId)
|
||||
// Scroll to first unread message when direct chat will be loaded
|
||||
shouldAutoScroll = true
|
||||
}
|
||||
) {
|
||||
MemberImage(member)
|
||||
}
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
Spacer(Modifier.size(4.dp))
|
||||
} 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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (sent) 76.dp else 12.dp,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (cItem.isRcvNew) {
|
||||
LaunchedEffect(cItem.id) {
|
||||
scope.launch {
|
||||
delay(750)
|
||||
markRead(CC.ItemRange(cItem.id, cItem.id), null)
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
|
||||
val member = cItem.chatDir.groupMember
|
||||
val showMember = showMemberImage(member, prevItem)
|
||||
Row(Modifier.padding(start = 8.dp, end = 66.dp).then(swipeableModifier)) {
|
||||
if (showMember) {
|
||||
val contactId = member.memberContactId
|
||||
if (contactId == null) {
|
||||
MemberImage(member)
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
showMemberInfo(chat.chatInfo.groupInfo, member)
|
||||
}
|
||||
) {
|
||||
MemberImage(member)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.size(4.dp))
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
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, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (sent) 76.dp else 12.dp,
|
||||
end = if (sent) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall)
|
||||
}
|
||||
}
|
||||
|
||||
if (cItem.isRcvNew) {
|
||||
LaunchedEffect(cItem.id) {
|
||||
scope.launch {
|
||||
delay(750)
|
||||
markRead(CC.ItemRange(cItem.id, cItem.id), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, floatingButton, listState)
|
||||
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -538,7 +564,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
minUnreadItemId: Long,
|
||||
searchValue: State<String>,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
floatingButton: MutableState<@Composable () -> Unit>,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
listState: LazyListState
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -554,6 +580,11 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
firstVisibleIndex = it
|
||||
firstItemIsVisible = firstVisibleIndex == 0
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
// When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block
|
||||
// so separate them into two LaunchedEffects
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
@@ -577,17 +608,18 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
LaunchedEffect(bottomUnreadCount, firstItemIsVisible) {
|
||||
val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty()
|
||||
val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible
|
||||
floatingButton.value = bottomEndFloatingButton(
|
||||
bottomUnreadCount,
|
||||
showButtonWithCounter,
|
||||
showButtonWithArrow,
|
||||
onClickArrowDown = {
|
||||
scope.launch { listState.animateScrollToItem(0) }
|
||||
},
|
||||
onClickCounter = {
|
||||
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) }
|
||||
}
|
||||
)
|
||||
setFloatingButton(
|
||||
bottomEndFloatingButton(
|
||||
bottomUnreadCount,
|
||||
showButtonWithCounter,
|
||||
showButtonWithArrow,
|
||||
onClickArrowDown = {
|
||||
scope.launch { listState.animateScrollToItem(0) }
|
||||
},
|
||||
onClickCounter = {
|
||||
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) }
|
||||
}
|
||||
))
|
||||
}
|
||||
// Don't show top FAB if is in search
|
||||
if (searchValue.value.isNotEmpty()) return
|
||||
@@ -735,7 +767,7 @@ private fun bottomEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
private fun ViewConfiguration.bigTouchSlop(slop: Float = 80f) = object: ViewConfiguration {
|
||||
private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConfiguration {
|
||||
override val longPressTimeoutMillis
|
||||
get() =
|
||||
this@bigTouchSlop.longPressTimeoutMillis
|
||||
@@ -793,9 +825,10 @@ fun PreviewChatLayout() {
|
||||
chatItems = chatItems,
|
||||
searchValue,
|
||||
useLinkPreviews = true,
|
||||
chatModelIncognito = false,
|
||||
back = {},
|
||||
info = {},
|
||||
openDirectChat = {},
|
||||
showMemberInfo = {_, _ -> },
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
@@ -849,9 +882,10 @@ fun PreviewGroupChatLayout() {
|
||||
chatItems = chatItems,
|
||||
searchValue,
|
||||
useLinkPreviews = true,
|
||||
chatModelIncognito = false,
|
||||
back = {},
|
||||
info = {},
|
||||
openDirectChat = {},
|
||||
showMemberInfo = {_, _ -> },
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -31,7 +30,7 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo
|
||||
Modifier
|
||||
.padding(start = 4.dp, end = 2.dp)
|
||||
.size(36.dp),
|
||||
tint = if (isSystemInDarkTheme()) FileDark else FileLight
|
||||
tint = if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
Text(fileName)
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
+43
-7
@@ -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
|
||||
@@ -93,7 +96,7 @@ fun AddGroupMembersLayout(
|
||||
ChatInfoToolbarTitle(
|
||||
ChatInfo.Group(groupInfo),
|
||||
imageSize = 60.dp,
|
||||
iconColor = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight
|
||||
iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight
|
||||
)
|
||||
}
|
||||
SectionSpacer()
|
||||
@@ -125,7 +128,7 @@ fun AddGroupMembersLayout(
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, addContact, removeContact)
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, addContact, removeContact)
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
@@ -255,6 +258,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 +266,7 @@ fun ContactList(
|
||||
contacts.forEachIndexed { index, contact ->
|
||||
SectionItemView {
|
||||
ContactCheckRow(
|
||||
contact, addContact, removeContact,
|
||||
contact, groupInfo, addContact, removeContact,
|
||||
checked = selectedContacts.contains(contact.apiId)
|
||||
)
|
||||
}
|
||||
@@ -276,14 +280,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 +317,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() {
|
||||
|
||||
+60
-15
@@ -11,11 +11,12 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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
|
||||
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.*
|
||||
|
||||
@@ -47,7 +49,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
AddGroupMembersView(groupInfo, chatModel, close)
|
||||
}
|
||||
@@ -56,13 +58,13 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
},
|
||||
showMemberInfo = { member ->
|
||||
withApi {
|
||||
val connStats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
ModalManager.shared.showCustomModal { closeCurrent ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
close = closeCurrent, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
GroupMemberInfoView(groupInfo, member, connStats, chatModel, close)
|
||||
GroupMemberInfoView(groupInfo, member, stats, chatModel, closeCurrent) { closeCurrent(); close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +74,10 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
},
|
||||
deleteGroup = { deleteGroupDialog(chat.chatInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }
|
||||
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
|
||||
changeNtfsState = { enabled ->
|
||||
changeNtfsState(enabled, chat, chatModel)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +127,7 @@ fun GroupChatInfoLayout(
|
||||
deleteGroup: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
leaveGroup: () -> Unit,
|
||||
changeNtfsState: (Boolean) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -133,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()
|
||||
}
|
||||
@@ -154,6 +162,17 @@ fun GroupChatInfoLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
var ntfsEnabled by remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
SectionView(title = stringResource(R.string.settings_section_title_settings)) {
|
||||
SectionItemView {
|
||||
NtfsSwitch(ntfsEnabled) {
|
||||
ntfsEnabled = !ntfsEnabled
|
||||
changeNtfsState(ntfsEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.canEdit) {
|
||||
SectionItemView {
|
||||
@@ -191,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()
|
||||
@@ -201,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,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(
|
||||
@@ -322,7 +366,8 @@ fun PreviewGroupChatInfoLayout() {
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {},
|
||||
changeNtfsState = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+54
-2
@@ -24,10 +24,18 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.SimplexServers
|
||||
import chat.simplex.app.views.chatlist.openChat
|
||||
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,
|
||||
closeAll: () -> Unit, // Close all open windows up to ChatView
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
@@ -37,6 +45,22 @@ fun GroupMemberInfoView(groupInfo: GroupInfo, member: GroupMember, connStats: Co
|
||||
member,
|
||||
connStats,
|
||||
developerTools,
|
||||
openDirectChat = {
|
||||
withApi {
|
||||
val oldChat = chatModel.getContactChat(member.memberContactId ?: return@withApi)
|
||||
if (oldChat != null) {
|
||||
openChat(oldChat.chatInfo, chatModel)
|
||||
} else {
|
||||
var newChat = chatModel.controller.apiGetChat(ChatType.Direct, member.memberContactId) ?: return@withApi
|
||||
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
|
||||
newChat = newChat.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
|
||||
chatModel.addChat(newChat)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatId.value = newChat.id
|
||||
}
|
||||
closeAll()
|
||||
}
|
||||
},
|
||||
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) }
|
||||
)
|
||||
}
|
||||
@@ -65,6 +89,7 @@ fun GroupMemberInfoLayout(
|
||||
member: GroupMember,
|
||||
connStats: ConnectionStats?,
|
||||
developerTools: Boolean,
|
||||
openDirectChat: () -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
@@ -81,6 +106,13 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
OpenChatButton(openDirectChat)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
|
||||
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
|
||||
val conn = member.activeConn
|
||||
@@ -139,7 +171,7 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
Modifier.padding(horizontal = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ProfileImage(size = 192.dp, member.image, color = if (isSystemInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Text(
|
||||
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
@@ -175,6 +207,25 @@ fun RemoveMemberButton(removeMember: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OpenChatButton(onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clickable { onClick() },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Message,
|
||||
stringResource(R.string.button_send_direct_message),
|
||||
Modifier.padding(top = 5.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.button_send_direct_message), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewGroupMemberInfoLayout() {
|
||||
@@ -184,6 +235,7 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
member = GroupMember.sampleData,
|
||||
connStats = null,
|
||||
developerTools = false,
|
||||
openDirectChat = {},
|
||||
removeMember = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -39,7 +38,7 @@ fun CIFileView(
|
||||
@Composable
|
||||
fun fileIcon(
|
||||
innerIcon: ImageVector? = null,
|
||||
color: Color = if (isSystemInDarkTheme()) FileDark else FileLight
|
||||
color: Color = if (isInDarkTheme()) FileDark else FileLight
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -105,7 +104,7 @@ fun CIFileView(
|
||||
fun progressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(32.dp),
|
||||
color = if (isSystemInDarkTheme()) FileDark else FileLight,
|
||||
color = if (isInDarkTheme()) FileDark else FileLight,
|
||||
strokeWidth = 4.dp
|
||||
)
|
||||
}
|
||||
|
||||
+10
-12
@@ -3,7 +3,6 @@ package chat.simplex.app.views.chat.item
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -19,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.*
|
||||
@@ -29,6 +27,7 @@ fun CIGroupInvitationView(
|
||||
ci: ChatItem,
|
||||
groupInvitation: CIGroupInvitation,
|
||||
memberRole: GroupMemberRole,
|
||||
chatIncognito: Boolean = false,
|
||||
joinGroup: (Long) -> Unit
|
||||
) {
|
||||
val sent = ci.chatDir.sent
|
||||
@@ -38,8 +37,8 @@ fun CIGroupInvitationView(
|
||||
fun groupInfoView() {
|
||||
val p = groupInvitation.groupProfile
|
||||
val iconColor =
|
||||
if (action) MaterialTheme.colors.primary
|
||||
else if (isSystemInDarkTheme()) FileDark else FileLight
|
||||
if (action) if (chatIncognito) Indigo else MaterialTheme.colors.primary
|
||||
else if (isInDarkTheme()) FileDark else FileLight
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
@@ -47,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),
|
||||
@@ -72,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,
|
||||
) {
|
||||
@@ -100,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()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -16,7 +15,6 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimplexBlue
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
@@ -56,7 +54,7 @@ fun CIStatusView(status: CIStatus, metaColor: Color = HighOrLowlight) {
|
||||
Icon(Icons.Filled.WarningAmber, stringResource(R.string.icon_descr_sent_msg_status_send_failed), Modifier.height(12.dp), tint = Color.Yellow)
|
||||
}
|
||||
is CIStatus.RcvNew -> {
|
||||
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = SimplexBlue)
|
||||
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -184,13 +185,13 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteM
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
Button(onClick = {
|
||||
TextButton(onClick = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(R.string.for_me_only)) }
|
||||
if (chatItem.meta.editable) {
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Button(onClick = {
|
||||
TextButton(onClick = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(R.string.for_everybody)) }
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ fun FramedItemView(
|
||||
Modifier
|
||||
.padding(top = 6.dp, end = 4.dp)
|
||||
.size(22.dp),
|
||||
tint = if (isSystemInDarkTheme()) FileDark else FileLight
|
||||
tint = if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
}
|
||||
else -> ciQuotedMsgView(qi)
|
||||
|
||||
+46
-17
@@ -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,
|
||||
@@ -119,6 +119,7 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Bo
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
ClearChatAction(chat, chatModel, showMenu)
|
||||
DeleteContactAction(chat, chatModel, showMenu)
|
||||
}
|
||||
@@ -127,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)
|
||||
}
|
||||
@@ -136,6 +137,7 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
ClearChatAction(chat, chatModel, showMenu)
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(groupInfo, chatModel, showMenu)
|
||||
@@ -160,6 +162,18 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
if (ntfsEnabled) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
|
||||
if (ntfsEnabled) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications,
|
||||
onClick = {
|
||||
changeNtfsState(!ntfsEnabled, chat, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
@@ -200,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
|
||||
}
|
||||
)
|
||||
@@ -227,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
|
||||
@@ -277,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) }
|
||||
@@ -318,14 +335,14 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
Button(onClick = {
|
||||
TextButton(onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContactConnectionAlert(connection, chatModel)
|
||||
}) {
|
||||
Text(stringResource(R.string.delete_verb))
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Button(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
@@ -374,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)
|
||||
@@ -459,6 +484,8 @@ fun PreviewChatListNavLinkDirect() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
false,
|
||||
null,
|
||||
stopped = false
|
||||
)
|
||||
},
|
||||
@@ -494,6 +521,8 @@ fun PreviewChatListNavLinkGroup() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
false,
|
||||
null,
|
||||
stopped = false
|
||||
)
|
||||
},
|
||||
@@ -516,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,
|
||||
|
||||
@@ -2,13 +2,13 @@ package chat.simplex.app.views.chatlist
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.filled.NotificationsOff
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -23,11 +23,10 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.item.MarkdownText
|
||||
import chat.simplex.app.views.helpers.ChatInfoImage
|
||||
import chat.simplex.app.views.helpers.badgeLayout
|
||||
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
|
||||
@@ -71,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()
|
||||
}
|
||||
@@ -80,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(
|
||||
@@ -88,7 +87,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
metaText = ci.timestampText,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.body1.copy(color = if (isSystemInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
|
||||
style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
|
||||
)
|
||||
} else {
|
||||
when (cInfo) {
|
||||
@@ -98,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 -> {}
|
||||
}
|
||||
@@ -120,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)
|
||||
|
||||
@@ -134,6 +133,7 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
modifier = Modifier.padding(bottom = 5.dp)
|
||||
)
|
||||
val n = chat.chatStats.unreadCount
|
||||
val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group)
|
||||
if (n > 0) {
|
||||
Box(
|
||||
Modifier.padding(top = 24.dp),
|
||||
@@ -144,12 +144,27 @@ fun ChatPreviewView(chat: Chat, stopped: Boolean) {
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.background(if (stopped) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.background(if (stopped || showNtfsIcon) HighOrLowlight else MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.badgeLayout()
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp)
|
||||
)
|
||||
}
|
||||
} else if (showNtfsIcon) {
|
||||
Box(
|
||||
Modifier.padding(top = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.NotificationsOff,
|
||||
contentDescription = generalGetString(R.string.notifications),
|
||||
tint = HighOrLowlight,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp)
|
||||
.size(17.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
Box(
|
||||
@@ -163,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)
|
||||
@@ -200,6 +225,6 @@ fun ChatStatusImage(chat: Chat) {
|
||||
@Composable
|
||||
fun PreviewChatPreviewView() {
|
||||
SimpleXTheme {
|
||||
ChatPreviewView(Chat.sampleData, stopped = false)
|
||||
ChatPreviewView(Chat.sampleData, false, "", stopped = false)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
@@ -37,7 +36,7 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) {
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
Text(contactConnection.description, maxLines = 2, color = if (isSystemInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
Text(contactConnection.description, maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
}
|
||||
val ts = getTimestampText(contactConnection.updatedAt)
|
||||
Column(
|
||||
|
||||
+4
-6
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
@@ -11,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(
|
||||
@@ -31,9 +29,9 @@ 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 (isSystemInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
}
|
||||
val ts = getTimestampText(contactRequest.contactRequest.updatedAt)
|
||||
Column(
|
||||
|
||||
@@ -2,8 +2,10 @@ package chat.simplex.app.views.helpers
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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
|
||||
|
||||
@@ -44,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 {
|
||||
@@ -53,13 +56,13 @@ class AlertManager {
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText) }
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = {
|
||||
TextButton(onClick = {
|
||||
onDismiss?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(dismissText) }
|
||||
@@ -79,7 +82,7 @@ class AlertManager {
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText) }
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -13,8 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.ui.theme.ToolbarDark
|
||||
import chat.simplex.app.ui.theme.ToolbarLight
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun DefaultTopAppBar(
|
||||
@@ -39,7 +38,7 @@ fun DefaultTopAppBar(
|
||||
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), onSearchValueChanged)
|
||||
}
|
||||
},
|
||||
backgroundColor = if (isSystemInDarkTheme()) ToolbarDark else ToolbarLight,
|
||||
backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight,
|
||||
navigationIcon = navigationButton,
|
||||
buttons = if (!showSearch) buttons else emptyList(),
|
||||
centered = !showSearch
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.ui.theme.GroupDark
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun SectionView(title: String? = null, content: (@Composable () -> Unit)) {
|
||||
@@ -18,7 +18,7 @@ fun SectionView(title: String? = null, content: (@Composable () -> Unit)) {
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 5.dp), fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Surface(color = if (isSystemInDarkTheme()) GroupDark else MaterialTheme.colors.background) {
|
||||
Surface(color = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background) {
|
||||
Column(Modifier.padding(horizontal = 6.dp).fillMaxWidth()) { content() }
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,27 @@ fun SectionItemView(click: (() -> Unit)? = null, height: Dp = 46.dp, disabled: B
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionItemViewSpaceBetween(
|
||||
click: (() -> Unit)? = null,
|
||||
height: Dp = 46.dp,
|
||||
padding: PaddingValues = PaddingValues(horizontal = 8.dp),
|
||||
disabled: Boolean = false,
|
||||
content: (@Composable () -> Unit)
|
||||
) {
|
||||
val modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
Row(
|
||||
if (click == null || disabled) modifier else modifier.clickable(onClick = click),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionTextFooter(text: String) {
|
||||
Text(
|
||||
@@ -49,9 +70,9 @@ fun SectionTextFooter(text: String) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionCustomFooter(content: (@Composable () -> Unit)) {
|
||||
fun SectionCustomFooter(padding: PaddingValues = PaddingValues(start = 16.dp, end = 16.dp, top = 5.dp), content: (@Composable () -> Unit)) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 16.dp).padding(top = 5.dp)
|
||||
Modifier.padding(padding)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@@ -80,3 +101,26 @@ fun InfoRow(title: String, value: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) {
|
||||
SectionItemView {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
Text(title)
|
||||
Text(value,
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.widthIn(max = (configuration.screenWidthDp / 2).dp)
|
||||
.clickable(onClick = onClick),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -46,7 +48,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
AddGroupMembersView(groupInfo, chatModel, 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,7 +3,6 @@ package chat.simplex.app.views.onboarding
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -77,7 +76,7 @@ fun SimpleXInfoLayout(
|
||||
@Composable
|
||||
fun SimpleXLogo() {
|
||||
Image(
|
||||
painter = painterResource(if (isSystemInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
|
||||
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
|
||||
contentDescription = stringResource(R.string.image_descr_simplex_logo),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 20.dp)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionCustomFooter
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
@@ -10,11 +13,13 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -24,7 +29,10 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.godaddy.android.colorpicker.*
|
||||
|
||||
enum class AppIcon(val resId: Int) {
|
||||
DEFAULT(R.mipmap.icon),
|
||||
@@ -32,7 +40,9 @@ enum class AppIcon(val resId: Int) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppearanceView() {
|
||||
fun AppearanceView(
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
val appIcon = remember { mutableStateOf(findEnabledIcon()) }
|
||||
|
||||
fun setAppIcon(newIcon: AppIcon) {
|
||||
@@ -54,18 +64,33 @@ fun AppearanceView() {
|
||||
|
||||
AppearanceLayout(
|
||||
appIcon,
|
||||
changeIcon = ::setAppIcon
|
||||
changeIcon = ::setAppIcon,
|
||||
showThemeSelector = showCustomModal { _, close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) colors.background else SettingsBackgroundLight
|
||||
) { ThemeSelectorView() }
|
||||
},
|
||||
editPrimaryColor = { primary ->
|
||||
showCustomModal { _, close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isInDarkTheme()) colors.background else SettingsBackgroundLight
|
||||
) { ColorEditor(primary, close) }
|
||||
}()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable fun AppearanceLayout(
|
||||
icon: MutableState<AppIcon>,
|
||||
changeIcon: (AppIcon) -> Unit
|
||||
changeIcon: (AppIcon) -> Unit,
|
||||
showThemeSelector: () -> Unit,
|
||||
editPrimaryColor: (Color) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.appearance_settings),
|
||||
@@ -73,10 +98,7 @@ fun AppearanceView() {
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(stringResource(R.string.settings_section_title_icon)) {
|
||||
LazyRow(
|
||||
Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
LazyRow {
|
||||
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
|
||||
val item = AppIcon.values()[index]
|
||||
val mipmap = ContextCompat.getDrawable(LocalContext.current, item.resId)!!
|
||||
@@ -97,9 +119,82 @@ fun AppearanceView() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
SectionView(stringResource(R.string.settings_section_title_themes)) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
SectionItemViewSpaceBetween(showThemeSelector, padding = PaddingValues()) {
|
||||
Text(generalGetString(R.string.theme))
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
|
||||
SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }, padding = PaddingValues()) {
|
||||
val title = generalGetString(R.string.color_primary)
|
||||
Text(title)
|
||||
Icon(Icons.Filled.Circle, title, tint = colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentTheme.first.primary != LightColorPalette.primary) {
|
||||
SectionCustomFooter(PaddingValues(start = 7.dp, end = 7.dp, top = 5.dp)) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
ThemeManager.saveAndApplyPrimaryColor(LightColorPalette.primary)
|
||||
},
|
||||
) {
|
||||
Text(generalGetString(R.string.reset_color))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorEditor(
|
||||
initialColor: Color,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
var currentColor by remember { mutableStateOf(initialColor) }
|
||||
ColorPicker(initialColor) {
|
||||
currentColor = it
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
ThemeManager.saveAndApplyPrimaryColor(currentColor)
|
||||
close()
|
||||
},
|
||||
Modifier.align(Alignment.CenterHorizontally),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = currentColor)
|
||||
) {
|
||||
Text(generalGetString(R.string.save_color))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
|
||||
ClassicColorPicker(
|
||||
color = initialColor,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
showAlphaBar = false,
|
||||
onColorChanged = { color: HsvColor ->
|
||||
onColorChanged(color.toColor())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
|
||||
SimplexApp.context.packageManager.getComponentEnabledSetting(
|
||||
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
|
||||
@@ -112,7 +207,9 @@ fun PreviewAppearanceSettings() {
|
||||
SimpleXTheme {
|
||||
AppearanceLayout(
|
||||
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
|
||||
changeIcon = {}
|
||||
changeIcon = {},
|
||||
showThemeSelector = {},
|
||||
editPrimaryColor = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
-7
@@ -10,12 +10,14 @@ 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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.NetCfg
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@@ -25,16 +27,19 @@ fun NetworkAndServersView(
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
|
||||
) {
|
||||
val netCfg: MutableState<NetCfg> = remember { mutableStateOf(chatModel.controller.getNetCfg()) }
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.value.useSocksProxy) }
|
||||
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
|
||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
|
||||
NetworkAndServersLayout(
|
||||
developerTools = developerTools,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
showModal = showModal,
|
||||
showSettingsModal = showSettingsModal,
|
||||
toggleSocksProxy = { enable ->
|
||||
toggleSocksProxy = { enable ->
|
||||
if (enable) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.network_enable_socks),
|
||||
@@ -45,6 +50,7 @@ fun NetworkAndServersView(
|
||||
chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults)
|
||||
chatModel.controller.setNetCfg(NetCfg.proxyDefaults)
|
||||
networkUseSocksProxy.value = true
|
||||
onionHosts.value = NetCfg.proxyDefaults.onionHosts
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -58,10 +64,29 @@ fun NetworkAndServersView(
|
||||
chatModel.controller.apiSetNetworkConfig(NetCfg.defaults)
|
||||
chatModel.controller.setNetCfg(NetCfg.defaults)
|
||||
networkUseSocksProxy.value = false
|
||||
onionHosts.value = NetCfg.defaults.onionHosts
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
useOnion = {
|
||||
val prevValue = onionHosts.value
|
||||
onionHosts.value = it
|
||||
updateNetworkSettingsDialog(onDismiss = {
|
||||
onionHosts.value = prevValue
|
||||
}) {
|
||||
withApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
onionHosts.value = it
|
||||
} else {
|
||||
onionHosts.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -69,9 +94,11 @@ fun NetworkAndServersView(
|
||||
@Composable fun NetworkAndServersLayout(
|
||||
developerTools: Boolean,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
toggleSocksProxy: (Boolean) -> Unit
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -89,6 +116,10 @@ fun NetworkAndServersView(
|
||||
SectionItemView {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, useOnion)
|
||||
}
|
||||
if (developerTools) {
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
@@ -129,6 +160,116 @@ fun UseSocksProxySwitch(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseOnionHosts(onionHosts: MutableState<OnionHosts>, enabled: State<Boolean>, useOnion: (OnionHosts) -> Unit) {
|
||||
val values = remember {
|
||||
OnionHosts.values().map {
|
||||
when (it) {
|
||||
OnionHosts.NEVER -> OnionHosts.NEVER to generalGetString(R.string.network_use_onion_hosts_no)
|
||||
OnionHosts.PREFER -> OnionHosts.PREFER to generalGetString(R.string.network_use_onion_hosts_prefer)
|
||||
OnionHosts.REQUIRED -> OnionHosts.REQUIRED to generalGetString(R.string.network_use_onion_hosts_required)
|
||||
}
|
||||
}
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.network_use_onion_hosts),
|
||||
values,
|
||||
onionHosts,
|
||||
icon = Icons.Outlined.Security,
|
||||
enabled = enabled,
|
||||
onSelected = useOnion
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ExposedDropDownSettingRow(
|
||||
title: String,
|
||||
values: List<Pair<T, String>>,
|
||||
selection: State<T>,
|
||||
label: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
iconTint: Color = HighOrLowlight,
|
||||
enabled: State<Boolean> = mutableStateOf(true),
|
||||
onSelected: (T) -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
icon,
|
||||
"",
|
||||
Modifier.padding(end = 8.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
}
|
||||
Text(title, color = if (enabled.value) Color.Unspecified else HighOrLowlight)
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded && enabled.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Text(
|
||||
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Icon(
|
||||
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
|
||||
generalGetString(R.string.icon_descr_more_button),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = 200.dp),
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
values.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onSelected(selectionOption.first)
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
selectionOption.second + (if (label != null) " $label" else ""),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNetworkSettingsDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.update_network_settings_question),
|
||||
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = onConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewNetworkAndServersLayout() {
|
||||
@@ -138,7 +279,9 @@ fun PreviewNetworkAndServersLayout() {
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
toggleSocksProxy = {}
|
||||
toggleSocksProxy = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
useOnion = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+78
-22
@@ -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,
|
||||
@@ -58,24 +62,12 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showSettingsModal = { modalView -> { ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(close = close, modifier = Modifier,
|
||||
background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) {
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) {
|
||||
modalView(chatModel)
|
||||
}
|
||||
} } },
|
||||
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } },
|
||||
showAppearance = {
|
||||
withApi {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalView(
|
||||
close = close, modifier = Modifier,
|
||||
background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
AppearanceView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// showVideoChatPrototype = { ModalManager.shared.showCustomModal { close -> CallViewDebug(close) } },
|
||||
)
|
||||
}
|
||||
@@ -88,7 +80,7 @@ val simplexTeamUri =
|
||||
//fun showSectionedModal(chatModel: ChatModel, modalView: (@Composable (ChatModel) -> Unit)) {
|
||||
// ModalManager.shared.showCustomModal { close ->
|
||||
// ModalView(close = close, modifier = Modifier,
|
||||
// background = if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) {
|
||||
// background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) {
|
||||
// modalView(chatModel)
|
||||
// }
|
||||
// }
|
||||
@@ -96,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,
|
||||
@@ -106,7 +100,6 @@ fun SettingsLayout(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
showTerminal: () -> Unit,
|
||||
showAppearance: () -> Unit
|
||||
// showVideoChatPrototype: () -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -114,7 +107,7 @@ fun SettingsLayout(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(if (isSystemInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight)
|
||||
.background(if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight)
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
@@ -129,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)
|
||||
@@ -142,7 +137,7 @@ fun SettingsLayout(
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showAppearance, disabled = stopped)
|
||||
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(showCustomModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped)
|
||||
}
|
||||
@@ -176,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(
|
||||
@@ -288,7 +324,7 @@ fun SettingsLayout(
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal))
|
||||
Text(generalGetString(R.string.install_simplex_chat_for_terminal), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,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,
|
||||
@@ -345,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 = {},
|
||||
@@ -355,7 +412,6 @@ fun PreviewSettingsLayout() {
|
||||
showSettingsModal = { {} },
|
||||
showCustomModal = { {} },
|
||||
showTerminal = {},
|
||||
showAppearance = {},
|
||||
// showVideoChatPrototype = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun ThemeSelectorView() {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val allThemes by remember { mutableStateOf(ThemeManager.allThemes(darkTheme)) }
|
||||
|
||||
ThemeSelectorLayout(
|
||||
allThemes,
|
||||
onSelectTheme = {
|
||||
ThemeManager.applyTheme(it, darkTheme)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable fun ThemeSelectorLayout(
|
||||
allThemes: List<Triple<Colors, DefaultTheme, String>>,
|
||||
onSelectTheme: (String) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.settings_section_title_themes).lowercase().capitalize(Locale.current),
|
||||
Modifier.padding(start = 16.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
SectionView(null) {
|
||||
LazyColumn(
|
||||
Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
items(allThemes.size) { index ->
|
||||
val item = allThemes[index]
|
||||
val onClick = {
|
||||
onSelectTheme(item.second.name)
|
||||
}
|
||||
SectionItemViewSpaceBetween(onClick, padding = PaddingValues()) {
|
||||
Text(item.third)
|
||||
if (currentTheme.second == item.second) {
|
||||
Icon(Icons.Outlined.Check, item.third, tint = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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 -->
|
||||
@@ -141,10 +145,14 @@
|
||||
<string name="file_not_found">Файл не найден</string>
|
||||
<string name="error_saving_file">Ошибка сохранения файла</string>
|
||||
|
||||
<!-- Chat Info Settings - ChatInfoView.kt -->
|
||||
<string name="notifications">Уведомления</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<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>
|
||||
@@ -157,9 +165,10 @@
|
||||
<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>
|
||||
|
||||
<!-- NewChatSheet -->
|
||||
<string name="add_contact_or_create_group">Начать новый разговор</string>
|
||||
@@ -195,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 -->
|
||||
@@ -204,6 +214,10 @@
|
||||
<string name="clear_chat_button">Очистить чат</string>
|
||||
<string name="mark_read">Прочитано</string>
|
||||
|
||||
<!-- Actions - ChatListNavLinkView.kt -->
|
||||
<string name="mute_chat">Без звука</string>
|
||||
<string name="unmute_chat">Уведомлять</string>
|
||||
|
||||
<!-- Pending contact connection alert dialogues -->
|
||||
<string name="you_invited_your_contact">Вы пригласили ваш контакт</string>
|
||||
<string name="you_accepted_connection">Вы приняли приглашение соединиться</string>
|
||||
@@ -243,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>
|
||||
@@ -264,7 +280,7 @@
|
||||
<string name="chat_lock">Блокировка SimpleX</string>
|
||||
<string name="chat_console">Консоль</string>
|
||||
<string name="smp_servers">SMP серверы</string>
|
||||
<string name="install_simplex_chat_for_terminal"><font color="#0088ff"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> для терминала</font></string>
|
||||
<string name="install_simplex_chat_for_terminal"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> для терминала</string>
|
||||
<string name="use_simplex_chat_servers__question">Использовать серверы предосталенные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>?</string>
|
||||
<string name="saved_SMP_servers_will_br_removed">Сохраненные SMP серверы будут удалены.</string>
|
||||
<string name="your_SMP_servers">Ваши SMP серверы</string>
|
||||
@@ -273,7 +289,7 @@
|
||||
<string name="enter_one_SMP_server_per_line">Введите SMP серверы, каждый сервер в отдельной строке:</string>
|
||||
<string name="how_to">Инфо</string>
|
||||
<string name="save_servers_button">Сохранить</string>
|
||||
<string name="network_and_servers">Сеть & серверы</string>
|
||||
<string name="network_and_servers">Сеть и серверы</string>
|
||||
<string name="network_settings">Настройки сети</string>
|
||||
<string name="network_settings_title">Настройки сети</string>
|
||||
<string name="network_socks_toggle">Использовать SOCKS прокси (порт 9050)</string>
|
||||
@@ -281,6 +297,10 @@
|
||||
<string name="network_enable_socks_info">Соединяться с серверами через SOCKS прокси через порт 9050? Прокси должен быть запущен до включения этой опции.</string>
|
||||
<string name="network_disable_socks">Использовать прямое соединение с Интернет?</string>
|
||||
<string name="network_disable_socks_info">Если вы подтвердите, серверы смогут видеть ваш IP адрес, а провайдер - с какими серверами вы соединяетесь.</string>
|
||||
<string name="network_use_onion_hosts">Использовать .onion хосты</string>
|
||||
<string name="network_use_onion_hosts_prefer">Когда возможно</string>
|
||||
<string name="network_use_onion_hosts_no">Нет</string>
|
||||
<string name="network_use_onion_hosts_required">Обязательно</string>
|
||||
<string name="appearance_settings">Интерфейс</string>
|
||||
|
||||
<!-- Address Items - UserAddressView.kt -->
|
||||
@@ -460,6 +480,8 @@
|
||||
<string name="settings_experimental_features">Экспериментальные функции</string>
|
||||
<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>
|
||||
@@ -510,6 +532,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>
|
||||
@@ -522,11 +545,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>
|
||||
@@ -572,6 +598,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>
|
||||
@@ -590,6 +618,7 @@
|
||||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member">Удалить члена группы</string>
|
||||
<string name="button_send_direct_message">Отправить сообщение</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Член группы будет удален - это действие нельзя отменить!</string>
|
||||
<string name="remove_member_confirmation">Удалить</string>
|
||||
<string name="member_info_section_title_member">ЧЛЕН ГРУППЫ</string>
|
||||
@@ -609,6 +638,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>
|
||||
@@ -627,4 +658,26 @@
|
||||
<string name="update_network_settings_question">Обновить настройки сети?</string>
|
||||
<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>
|
||||
<string name="theme_dark">Темная</string>
|
||||
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Тема</string>
|
||||
<string name="save_color">Сохранить цвет</string>
|
||||
<string name="reset_color">Сбросить цвета</string>
|
||||
<string name="color_primary">Акцент</string>
|
||||
</resources>
|
||||
|
||||
@@ -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 -->
|
||||
@@ -141,10 +145,14 @@
|
||||
<string name="file_not_found">File not found</string>
|
||||
<string name="error_saving_file">Error saving file</string>
|
||||
|
||||
<!-- Chat Info Settings - ChatInfoView.kt -->
|
||||
<string name="notifications">Notifications</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<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>
|
||||
@@ -157,9 +165,10 @@
|
||||
<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>
|
||||
|
||||
<!-- NewChatSheet -->
|
||||
<string name="add_contact_or_create_group">Start new chat</string>
|
||||
@@ -195,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 -->
|
||||
@@ -204,6 +214,10 @@
|
||||
<string name="clear_chat_button">Clear chat</string>
|
||||
<string name="mark_read">Mark read</string>
|
||||
|
||||
<!-- Actions - ChatListNavLinkView.kt -->
|
||||
<string name="mute_chat">Mute</string>
|
||||
<string name="unmute_chat">Unmute</string>
|
||||
|
||||
<!-- Pending contact connection alert dialogues -->
|
||||
<string name="you_invited_your_contact">You invited your contact</string>
|
||||
<string name="you_accepted_connection">You accepted connection</string>
|
||||
@@ -243,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>
|
||||
@@ -269,7 +284,7 @@
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="smp_servers">SMP servers</string>
|
||||
<string name="install_simplex_chat_for_terminal">Install <font color="#0088ff"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</font></string>
|
||||
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
|
||||
<string name="use_simplex_chat_servers__question">Use <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers?</string>
|
||||
<string name="saved_SMP_servers_will_br_removed">Saved SMP servers will be removed.</string>
|
||||
<string name="your_SMP_servers">Your SMP servers</string>
|
||||
@@ -286,6 +301,10 @@
|
||||
<string name="network_enable_socks_info">Access the servers via SOCKS proxy on port 9050? Proxy must be started before enabling this option.</string>
|
||||
<string name="network_disable_socks">Use direct Internet connection?</string>
|
||||
<string name="network_disable_socks_info">If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to.</string>
|
||||
<string name="network_use_onion_hosts">Use .onion hosts</string>
|
||||
<string name="network_use_onion_hosts_prefer">When available</string>
|
||||
<string name="network_use_onion_hosts_no">No</string>
|
||||
<string name="network_use_onion_hosts_required">Required</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
|
||||
<!-- Address Items - UserAddressView.kt -->
|
||||
@@ -462,6 +481,8 @@
|
||||
<string name="settings_experimental_features">Experimental features</string>
|
||||
<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>
|
||||
@@ -512,6 +533,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>
|
||||
@@ -524,11 +546,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>
|
||||
@@ -574,6 +599,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>
|
||||
@@ -592,6 +619,7 @@
|
||||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member">Remove member</string>
|
||||
<string name="button_send_direct_message">Send direct message</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
|
||||
<string name="remove_member_confirmation">Remove</string>
|
||||
<string name="member_info_section_title_member">MEMBER</string>
|
||||
@@ -611,6 +639,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>
|
||||
@@ -629,4 +660,26 @@
|
||||
<string name="update_network_settings_question">Update network settings?</string>
|
||||
<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>
|
||||
<string name="theme_dark">Dark</string>
|
||||
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Theme</string>
|
||||
<string name="save_color">Save color</string>
|
||||
<string name="reset_color">Reset colors</string>
|
||||
<string name="color_primary">Accent</string>
|
||||
</resources>
|
||||
|
||||
@@ -80,6 +80,16 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
terminateChat()
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
configurationForConnecting connectingSceneSession: UISceneSession,
|
||||
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
|
||||
if connectingSceneSession.role == .windowApplication {
|
||||
configuration.delegateClass = SceneDelegate.self
|
||||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
private func receiveMessages(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
let complete = BGManager.shared.completionHandler {
|
||||
logger.debug("AppDelegate: completed BGManager.receiveMessages")
|
||||
@@ -89,3 +99,14 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
BGManager.shared.receiveMessages(complete)
|
||||
}
|
||||
}
|
||||
|
||||
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
window = windowScene.keyWindow
|
||||
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
|
||||
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var tokenStatus: NtfTknStatus?
|
||||
@Published var notificationMode = NotificationsMode.off
|
||||
@Published var notificationPreview: NotificationPreviewMode? = ntfPreviewModeGroupDefault.get()
|
||||
@Published var incognito: Bool = incognitoGroupDefault.get()
|
||||
// pending notification actions
|
||||
@Published var ntfContactRequest: ChatId?
|
||||
@Published var ntfCallInvitationAction: (ChatId, NtfCallAction)?
|
||||
@@ -58,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 })
|
||||
}
|
||||
@@ -79,7 +90,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateContact(_ contact: Contact) {
|
||||
updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact())
|
||||
updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact)
|
||||
}
|
||||
|
||||
func updateGroup(_ groupInfo: GroupInfo) {
|
||||
|
||||
@@ -167,6 +167,12 @@ func apiSetFilesFolder(filesFolder: String) throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetIncognito(incognito: Bool) throws {
|
||||
let r = chatSendCmdSync(.setIncognito(incognito: incognito))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiExportArchive(config: ArchiveConfig) async throws {
|
||||
try await sendCommandOkResp(.apiExportArchive(config: config))
|
||||
}
|
||||
@@ -200,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 {
|
||||
@@ -312,15 +319,15 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
|
||||
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
|
||||
}
|
||||
|
||||
func apiContactInfo(contactId: Int64) async throws -> ConnectionStats? {
|
||||
func apiContactInfo(contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
|
||||
let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
|
||||
if case let .contactInfo(_, connStats) = r { return connStats }
|
||||
if case let .contactInfo(_, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> ConnectionStats? {
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?) {
|
||||
let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return connStats_ }
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -429,6 +436,12 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? {
|
||||
let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias))
|
||||
if case let .contactAliasUpdated(toContact) = r { return toContact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
let r = await chatSendCmd(.createMyAddress)
|
||||
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
||||
@@ -646,6 +659,7 @@ func initializeChat(start: Bool) throws {
|
||||
do {
|
||||
let m = ChatModel.shared
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetIncognito(incognito: incognitoGroupDefault.get())
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
if m.currentUser == nil {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
|
||||
@@ -19,6 +19,10 @@ struct ChatInfoToolbar: View {
|
||||
var body: some View {
|
||||
let cInfo = chat.chatInfo
|
||||
return HStack {
|
||||
if (cInfo.incognito) {
|
||||
Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo)
|
||||
Spacer().frame(width: 16)
|
||||
}
|
||||
ChatInfoImage(
|
||||
chat: chat,
|
||||
color: colorScheme == .dark
|
||||
|
||||
@@ -46,7 +46,11 @@ struct ChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
var contact: Contact
|
||||
var connectionStats: ConnectionStats?
|
||||
var customUserProfile: Profile?
|
||||
@State var localAlias: String
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
@State private var alert: ChatInfoViewAlert? = nil
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
@@ -63,6 +67,20 @@ struct ChatInfoView: View {
|
||||
List {
|
||||
contactInfoHeader()
|
||||
.listRowBackground(Color.clear)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
aliasTextFieldFocused = false
|
||||
}
|
||||
|
||||
localAliasTextEdit()
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
if let customUserProfile = customUserProfile {
|
||||
Section("Incognito") {
|
||||
infoRow("Your random profile", customUserProfile.chatViewName)
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
@@ -106,11 +124,11 @@ struct ChatInfoView: View {
|
||||
.frame(width: 192, height: 192)
|
||||
.padding(.top, 12)
|
||||
.padding()
|
||||
Text(cInfo.displayName)
|
||||
Text(contact.profile.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, 2)
|
||||
if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName {
|
||||
if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName {
|
||||
Text(cInfo.fullName)
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
@@ -119,6 +137,37 @@ struct ChatInfoView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
func localAliasTextEdit() -> some View {
|
||||
TextField("Set contact name…", text: $localAlias)
|
||||
.disableAutocorrection(true)
|
||||
.focused($aliasTextFieldFocused)
|
||||
.submitLabel(.done)
|
||||
.onChange(of: aliasTextFieldFocused) { focused in
|
||||
if !focused {
|
||||
setContactAlias()
|
||||
}
|
||||
}
|
||||
.onSubmit {
|
||||
setContactAlias()
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func setContactAlias() {
|
||||
Task {
|
||||
do {
|
||||
if let contact = try await apiSetContactAlias(contactId: chat.chatInfo.apiId, localAlias: localAlias) {
|
||||
await MainActor.run {
|
||||
chatModel.updateContact(contact)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("setContactAlias error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func networkStatusRow() -> some View {
|
||||
HStack {
|
||||
Text("Network status")
|
||||
@@ -167,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 {
|
||||
@@ -202,6 +252,6 @@ struct ChatInfoView: View {
|
||||
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, localAlias: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIGroupInvitationView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var chatItem: ChatItem
|
||||
var groupInvitation: CIGroupInvitation
|
||||
var memberRole: GroupMemberRole
|
||||
var chatIncognito: Bool = false
|
||||
@State private var frameWidth: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
@@ -29,10 +31,13 @@ struct CIGroupInvitationView: View {
|
||||
Divider().frame(width: frameWidth)
|
||||
|
||||
if action {
|
||||
groupInvitationText().overlay(DetermineWidth())
|
||||
Text("Tap to join")
|
||||
.foregroundColor(.accentColor)
|
||||
groupInvitationText()
|
||||
.overlay(DetermineWidth())
|
||||
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
||||
.foregroundColor(chatIncognito ? .indigo : .accentColor)
|
||||
.font(.callout)
|
||||
.padding(.trailing, 60)
|
||||
.overlay(DetermineWidth())
|
||||
} else {
|
||||
groupInvitationText()
|
||||
.padding(.trailing, 60)
|
||||
@@ -52,17 +57,26 @@ struct CIGroupInvitationView: View {
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||
|
||||
if action {
|
||||
v.onTapGesture { acceptInvitation() }
|
||||
v.onTapGesture {
|
||||
joinGroup(groupInvitation.groupId)
|
||||
}
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
private func groupInfoView(_ action: Bool) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
var color: Color
|
||||
if action {
|
||||
color = chatIncognito ? .indigo : .accentColor
|
||||
} else {
|
||||
color = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
return HStack(alignment: .top) {
|
||||
ProfileImage(
|
||||
imageStr: groupInvitation.groupProfile.image,
|
||||
iconName: "person.2.circle.fill",
|
||||
color: action ? .accentColor : Color(uiColor: .tertiaryLabel)
|
||||
color: color
|
||||
)
|
||||
.frame(width: 44, height: 44)
|
||||
.padding(.trailing, 4)
|
||||
@@ -94,13 +108,6 @@ struct CIGroupInvitationView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func acceptInvitation() {
|
||||
Task {
|
||||
logger.debug("acceptInvitation")
|
||||
await joinGroup(groupInvitation.groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CIGroupInvitationView_Previews: PreviewProvider {
|
||||
|
||||
@@ -48,7 +48,7 @@ struct ChatItemView: View {
|
||||
}
|
||||
|
||||
private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View {
|
||||
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole)
|
||||
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito)
|
||||
}
|
||||
|
||||
private func groupEventItemView() -> some View {
|
||||
|
||||
@@ -22,6 +22,7 @@ struct ChatView: View {
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State private var showDeleteMessage = false
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var customUserProfile: Profile?
|
||||
@State private var tableView: UITableView?
|
||||
@State private var loadingItems = false
|
||||
@State private var firstPage = false
|
||||
@@ -30,6 +31,9 @@ struct ChatView: View {
|
||||
@State private var searchMode = false
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocussed
|
||||
// opening GroupMemberInfoView on member icon
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@State private var memberConnectionStats: ConnectionStats?
|
||||
|
||||
var body: some View {
|
||||
let cInfo = chat.chatInfo
|
||||
@@ -68,22 +72,38 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "chevron.backward")
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "chevron.backward")
|
||||
Text("Chats")
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Button {
|
||||
if case .direct = cInfo {
|
||||
if case let .direct(contact) = cInfo {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
let stats = try await apiContactInfo(contactId: chat.chatInfo.apiId)
|
||||
await MainActor.run { connectionStats = stats }
|
||||
let (stats, profile) = try await apiContactInfo(contactId: chat.chatInfo.apiId)
|
||||
await MainActor.run {
|
||||
connectionStats = stats
|
||||
customUserProfile = profile
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiContactInfo error: \(responseError(error))")
|
||||
}
|
||||
await MainActor.run { showChatInfoSheet = true }
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $showChatInfoSheet, onDismiss: {
|
||||
connectionStats = nil
|
||||
customUserProfile = nil
|
||||
}) {
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Button {
|
||||
Task {
|
||||
let groupMembers = await apiListMembers(groupInfo.groupId)
|
||||
await MainActor.run {
|
||||
@@ -91,18 +111,11 @@ struct ChatView: View {
|
||||
showChatInfoSheet = true
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $showChatInfoSheet) {
|
||||
switch cInfo {
|
||||
case .direct:
|
||||
ChatInfoView(chat: chat, connectionStats: connectionStats)
|
||||
case let .group(groupInfo):
|
||||
.sheet(isPresented: $showChatInfoSheet) {
|
||||
GroupChatInfoView(chat: chat, groupInfo: groupInfo)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,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()
|
||||
@@ -219,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)
|
||||
@@ -338,13 +366,28 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
|
||||
if case let .groupRcv(member) = ci.chatDir {
|
||||
if case let .groupRcv(member) = ci.chatDir,
|
||||
case let .group(groupInfo) = chat.chatInfo {
|
||||
let prevItem = chatModel.getPrevChatItem(ci)
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
let showMember = prevItem == nil || showMemberImage(member, prevItem)
|
||||
if showMember {
|
||||
ProfileImage(imageStr: member.memberProfile.image)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
.onTapGesture {
|
||||
Task {
|
||||
do {
|
||||
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 }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats)
|
||||
}
|
||||
} else {
|
||||
Rectangle().fill(.clear)
|
||||
.frame(width: memberImageSize, height: memberImageSize)
|
||||
|
||||
@@ -15,9 +15,23 @@ 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
|
||||
@State private var alert: AddGroupMembersAlert?
|
||||
|
||||
private enum AddGroupMembersAlert: Identifiable {
|
||||
case prohibitedToInviteIncognito
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .prohibitedToInviteIncognito: return "prohibitedToInviteIncognito"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@@ -42,15 +56,17 @@ struct AddGroupMembersView: View {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,27 +93,22 @@ struct AddGroupMembersView: View {
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case .prohibitedToInviteIncognito:
|
||||
return Alert(
|
||||
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 let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text("\(error)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func inviteMembersButton() -> some View {
|
||||
private func inviteMembersButton() -> some View {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
for contactId in selectedContacts {
|
||||
let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole)
|
||||
await MainActor.run { _ = ChatModel.shared.upsertGroupMember(groupInfo, member) }
|
||||
}
|
||||
await MainActor.run { dismiss() }
|
||||
if let cb = addedMembersCb { cb(selectedContacts) }
|
||||
} catch {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text("Error adding member(s)"),
|
||||
message: Text(responseError(error))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
inviteMembers()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Invite to group")
|
||||
@@ -107,7 +118,22 @@ struct AddGroupMembersView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
||||
func rolePicker() -> some View {
|
||||
private func inviteMembers() {
|
||||
Task {
|
||||
do {
|
||||
for contactId in selectedContacts {
|
||||
let member = try await apiAddMember(groupInfo.groupId, contactId, selectedRole)
|
||||
await MainActor.run { _ = ChatModel.shared.upsertGroupMember(groupInfo, member) }
|
||||
}
|
||||
await MainActor.run { dismiss() }
|
||||
if let cb = addedMembersCb { cb(selectedContacts) }
|
||||
} catch {
|
||||
alert = .error(title: "Error adding member(s)", error: responseError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rolePicker() -> some View {
|
||||
Picker("New member role", selection: $selectedRole) {
|
||||
ForEach(GroupMemberRole.allCases) { role in
|
||||
if role <= groupInfo.membership.memberRole {
|
||||
@@ -117,13 +143,32 @@ struct AddGroupMembersView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func contactCheckView(_ contact: Contact) -> some View {
|
||||
private func contactCheckView(_ contact: Contact) -> some View {
|
||||
let checked = selectedContacts.contains(contact.apiId)
|
||||
return Button {
|
||||
let prohibitedToInviteIncognito = !chat.chatInfo.incognito && contact.contactConnIncognito
|
||||
var icon: String
|
||||
var iconColor: Color
|
||||
if prohibitedToInviteIncognito {
|
||||
icon = "theatermasks.circle.fill"
|
||||
iconColor = Color(uiColor: .tertiaryLabel)
|
||||
} else {
|
||||
if checked {
|
||||
selectedContacts.remove(contact.apiId)
|
||||
icon = "checkmark.circle.fill"
|
||||
iconColor = .accentColor
|
||||
} else {
|
||||
selectedContacts.insert(contact.apiId)
|
||||
icon = "circle"
|
||||
iconColor = Color(uiColor: .tertiaryLabel)
|
||||
}
|
||||
}
|
||||
return Button {
|
||||
if prohibitedToInviteIncognito {
|
||||
alert = .prohibitedToInviteIncognito
|
||||
} else {
|
||||
if checked {
|
||||
selectedContacts.remove(contact.apiId)
|
||||
} else {
|
||||
selectedContacts.insert(contact.apiId)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack{
|
||||
@@ -131,11 +176,11 @@ struct AddGroupMembersView: View {
|
||||
.frame(width: 30, height: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(ChatInfo.direct(contact: contact).chatViewName)
|
||||
.foregroundColor(.primary)
|
||||
.foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
Image(systemName: checked ? "checkmark.circle.fill": "circle")
|
||||
.foregroundColor(checked ? .accentColor : Color(uiColor: .tertiaryLabel))
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(iconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ struct GroupChatInfoView: View {
|
||||
case deleteGroupAlert
|
||||
case clearChatAlert
|
||||
case leaveGroupAlert
|
||||
case cantInviteIncognitoAlert
|
||||
|
||||
var id: GroupChatInfoViewAlert { get { self } }
|
||||
}
|
||||
@@ -42,7 +43,13 @@ 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
|
||||
@@ -97,6 +104,8 @@ struct GroupChatInfoView: View {
|
||||
case .deleteGroupAlert: return deleteGroupAlert()
|
||||
case .clearChatAlert: return clearChatAlert()
|
||||
case .leaveGroupAlert: return leaveGroupAlert()
|
||||
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +159,7 @@ struct GroupChatInfoView: View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(member.chatViewName)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.primary)
|
||||
.foregroundColor(member.memberIncognito ? .indigo : .primary)
|
||||
let s = Text(member.memberStatus.shortText)
|
||||
(user ? Text ("you: ") + s : s)
|
||||
.lineLimit(1)
|
||||
@@ -212,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 {
|
||||
@@ -252,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)
|
||||
|
||||
@@ -30,6 +30,12 @@ struct GroupMemberInfoView: View {
|
||||
groupMemberInfoHeader()
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
if let contactId = member.memberContactId {
|
||||
Section {
|
||||
openDirectChatButton(contactId)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
// TODO change role
|
||||
@@ -72,6 +78,30 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func openDirectChatButton(_ contactId: Int64) -> some View {
|
||||
Button {
|
||||
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 = chat.id
|
||||
}
|
||||
} label: {
|
||||
Label("Send direct message", systemImage: "message")
|
||||
}
|
||||
}
|
||||
|
||||
private func groupMemberInfoHeader() -> some View {
|
||||
VStack {
|
||||
ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -75,14 +69,16 @@ struct ChatListNavLink: View {
|
||||
ChatPreviewView(chat: chat)
|
||||
.frame(height: 80)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
joinGroupButton()
|
||||
joinGroupButton(groupInfo.hostConnCustomUserProfileId)
|
||||
if groupInfo.canDelete {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
}
|
||||
}
|
||||
.onTapGesture { showJoinGroupDialog = true }
|
||||
.confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) {
|
||||
Button("Join group") { Task { await joinGroup(groupInfo.groupId) } }
|
||||
Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") {
|
||||
joinGroup(groupInfo.groupId)
|
||||
}
|
||||
Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } }
|
||||
}
|
||||
case .memAccepted:
|
||||
@@ -95,7 +91,6 @@ struct ChatListNavLink: View {
|
||||
NavLinkPlain(
|
||||
tag: chat.chatInfo.id,
|
||||
selection: $chatModel.chatId,
|
||||
destination: { chatView() },
|
||||
label: { ChatPreviewView(chat: chat) },
|
||||
disabled: !groupInfo.ready
|
||||
)
|
||||
@@ -113,7 +108,7 @@ struct ChatListNavLink: View {
|
||||
} label: {
|
||||
Label("Leave", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
.tint(Color.indigo)
|
||||
.tint(Color.yellow)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
@@ -124,13 +119,13 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func joinGroupButton() -> some View {
|
||||
private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View {
|
||||
Button {
|
||||
Task { await joinGroup(chat.chatInfo.apiId) }
|
||||
joinGroup(chat.chatInfo.apiId)
|
||||
} label: {
|
||||
Label("Join", systemImage: "ipad.and.arrow.forward")
|
||||
Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
}
|
||||
|
||||
private func markReadButton() -> some View {
|
||||
@@ -162,9 +157,10 @@ struct ChatListNavLink: View {
|
||||
private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
|
||||
ContactRequestView(contactRequest: contactRequest, chat: chat)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button { Task { await acceptContactRequest(contactRequest) } }
|
||||
label: { Label("Accept", systemImage: "checkmark") }
|
||||
.tint(Color.accentColor)
|
||||
Button {
|
||||
Task { await acceptContactRequest(contactRequest) }
|
||||
} label: { Label("Accept", systemImage: chatModel.incognito ? "theatermasks" : "checkmark") }
|
||||
.tint(chatModel.incognito ? .indigo : .accentColor)
|
||||
Button(role: .destructive) {
|
||||
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest))
|
||||
} label: {
|
||||
@@ -174,7 +170,7 @@ struct ChatListNavLink: View {
|
||||
.frame(height: 80)
|
||||
.onTapGesture { showContactRequestDialog = true }
|
||||
.confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
|
||||
Button("Accept contact") { Task { await acceptContactRequest(contactRequest) } }
|
||||
Button(chatModel.incognito ? "Accept incognito" : "Accept contact") { Task { await acceptContactRequest(contactRequest) } }
|
||||
Button("Reject contact (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } }
|
||||
}
|
||||
}
|
||||
@@ -325,32 +321,35 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
func joinGroup(_ groupId: Int64) async {
|
||||
do {
|
||||
let r = try await apiJoinGroup(groupId)
|
||||
switch r {
|
||||
case let .joined(groupInfo):
|
||||
await MainActor.run { ChatModel.shared.updateGroup(groupInfo) }
|
||||
case .invitationRemoved:
|
||||
AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.")
|
||||
await deleteGroup()
|
||||
case .groupNotFound:
|
||||
AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.")
|
||||
await deleteGroup()
|
||||
}
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error joining group"), message: Text(err)))
|
||||
logger.error("apiJoinGroup error: \(err)")
|
||||
}
|
||||
|
||||
func deleteGroup() async {
|
||||
func joinGroup(_ groupId: Int64) {
|
||||
Task {
|
||||
logger.debug("joinGroup")
|
||||
do {
|
||||
// TODO this API should update chat item with the invitation as well
|
||||
try await apiDeleteChat(type: .group, id: groupId)
|
||||
await MainActor.run { ChatModel.shared.removeChat("#\(groupId)") }
|
||||
} catch {
|
||||
logger.error("apiDeleteChat error: \(responseError(error))")
|
||||
let r = try await apiJoinGroup(groupId)
|
||||
switch r {
|
||||
case let .joined(groupInfo):
|
||||
await MainActor.run { ChatModel.shared.updateGroup(groupInfo) }
|
||||
case .invitationRemoved:
|
||||
AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.")
|
||||
await deleteGroup()
|
||||
case .groupNotFound:
|
||||
AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.")
|
||||
await deleteGroup()
|
||||
}
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error joining group"), message: Text(err)))
|
||||
logger.error("apiJoinGroup error: \(err)")
|
||||
}
|
||||
|
||||
func deleteGroup() async {
|
||||
do {
|
||||
// TODO this API should update chat item with the invitation as well
|
||||
try await apiDeleteChat(type: .group, id: groupId)
|
||||
await MainActor.run { ChatModel.shared.removeChat("#\(groupId)") }
|
||||
} catch {
|
||||
logger.error("apiDeleteChat error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -44,6 +46,17 @@ struct ChatListView: View {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
SettingsButton()
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
if (chatModel.incognito) {
|
||||
HStack {
|
||||
if (chatModel.chats.count > 8) {
|
||||
Text("Your chats").font(.headline)
|
||||
Spacer().frame(width: 16)
|
||||
}
|
||||
Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo)
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
switch chatModel.chatRunning {
|
||||
case .some(true): NewChatButton()
|
||||
@@ -52,6 +65,15 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(
|
||||
NavigationLink(
|
||||
destination: chatView(selectedChat),
|
||||
isActive: Binding(
|
||||
get: { selectedChat != nil },
|
||||
set: { _, _ in selectedChat = nil }
|
||||
)
|
||||
) { EmptyView() }
|
||||
)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
@@ -62,13 +84,28 @@ 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 == ""
|
||||
? chatModel.chats
|
||||
: chatModel.chats.filter {
|
||||
$0.chatInfo.chatType != .contactConnection &&
|
||||
$0.chatInfo.chatViewName.localizedLowercase.contains(s)
|
||||
: chatModel.chats.filter { chat in
|
||||
let contains = chat.chatInfo.chatViewName.localizedLowercase.contains(s)
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
return contains
|
||||
|| contact.profile.displayName.localizedLowercase.contains(s)
|
||||
|| contact.fullName.localizedLowercase.contains(s)
|
||||
case .contactConnection: return false
|
||||
default: return contains
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatPreviewView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var darkGreen = Color(red: 0, green: 0.5, blue: 0)
|
||||
@@ -23,7 +24,6 @@ struct ChatPreviewView: View {
|
||||
.frame(width: 63, height: 63)
|
||||
chatPreviewImageOverlayIcon()
|
||||
.padding([.bottom, .trailing], 1)
|
||||
|
||||
}
|
||||
.padding(.leading, 4)
|
||||
|
||||
@@ -89,7 +89,7 @@ struct ChatPreviewView: View {
|
||||
case .group(groupInfo: let groupInfo):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited:
|
||||
v.foregroundColor(.accentColor)
|
||||
chat.chatInfo.incognito ? v.foregroundColor(.indigo) : v.foregroundColor(.accentColor)
|
||||
case .memAccepted:
|
||||
v.foregroundColor(.secondary)
|
||||
default: v
|
||||
@@ -127,7 +127,7 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited: chatPreviewInfoText("you are invited to group")
|
||||
case .memInvited: groupInvitationPreviewText(groupInfo)
|
||||
case .memAccepted: chatPreviewInfoText("connecting…")
|
||||
default: EmptyView()
|
||||
}
|
||||
@@ -136,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)
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ContactRequestView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
var contactRequest: UserContactRequest
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
@@ -17,12 +18,13 @@ struct ContactRequestView: View {
|
||||
return HStack(spacing: 8) {
|
||||
ChatInfoImage(chat: chat)
|
||||
.frame(width: 63, height: 63)
|
||||
.padding(.leading, 4)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .top) {
|
||||
Text(contactRequest.chatViewName)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
.foregroundColor(chatModel.incognito ? .indigo : .accentColor)
|
||||
.padding(.leading, 8)
|
||||
.padding(.top, 4)
|
||||
.frame(maxHeight: .infinity, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import CoreImage.CIFilterBuiltins
|
||||
|
||||
struct AddContactView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
var connReqInvitation: String
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -17,8 +18,23 @@ struct AddContactView: View {
|
||||
Text("One-time invitation link")
|
||||
.font(.title)
|
||||
.padding(.vertical)
|
||||
Text("Your contact can scan it from the app")
|
||||
Text("Your contact can scan it from the app.")
|
||||
.padding(.bottom, 4)
|
||||
if (chatModel.incognito) {
|
||||
HStack {
|
||||
Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("A random profile will be sent to your contact").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("Your chat profile will be sent to your contact").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
QRCode(uri: connReqInvitation)
|
||||
.padding(.bottom)
|
||||
Text("If you can't meet in person, **show QR code in the video call**, or share the link.")
|
||||
|
||||
@@ -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
|
||||
@@ -44,7 +47,21 @@ struct AddGroupView: View {
|
||||
.padding(.vertical, 4)
|
||||
Text("The group is fully decentralized – it is visible only to the members.")
|
||||
.padding(.bottom, 4)
|
||||
if (m.incognito) {
|
||||
HStack {
|
||||
Image(systemName: "info.circle").foregroundColor(.orange).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("Incognito mode is not supported here - your main profile will be sent to group members").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("Your chat profile will be sent to group members").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
ZStack(alignment: .center) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PasteToConnectView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State private var connectionLink: String = ""
|
||||
|
||||
@@ -18,8 +19,22 @@ struct PasteToConnectView: View {
|
||||
.font(.title)
|
||||
.padding(.vertical)
|
||||
Text("Paste the link you received into the box below to connect with your contact.")
|
||||
Text("Your profile will be sent to the contact that you received this link from")
|
||||
.padding(.bottom, 4)
|
||||
if (chatModel.incognito) {
|
||||
HStack {
|
||||
Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("A random profile will be sent to the contact that you received this link from").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("Your profile will be sent to the contact that you received this link from").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
TextEditor(text: $connectionLink)
|
||||
.onSubmit(connect)
|
||||
.textInputAutocapitalization(.never)
|
||||
@@ -55,7 +70,7 @@ struct PasteToConnectView: View {
|
||||
.frame(height: 48)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button")
|
||||
Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
|
||||
}
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import CodeScanner
|
||||
|
||||
struct ScanToConnectView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
@@ -17,8 +18,21 @@ struct ScanToConnectView: View {
|
||||
Text("Scan QR code")
|
||||
.font(.title)
|
||||
.padding(.vertical)
|
||||
Text("Your chat profile will be sent to your contact")
|
||||
if (chatModel.incognito) {
|
||||
HStack {
|
||||
Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("A random profile will be sent to your contact").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote)
|
||||
Spacer().frame(width: 8)
|
||||
Text("Your chat profile will be sent to your contact").font(.footnote)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
ZStack {
|
||||
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
|
||||
@@ -8,9 +8,18 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||
|
||||
let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
|
||||
|
||||
let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"]
|
||||
|
||||
struct AppearanceSettings: View {
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
@State private var iconLightTapped = false
|
||||
@State private var iconDarkTapped = false
|
||||
@State private var userInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
@State private var uiTintColor = getUIAccentColorDefault()
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
@@ -22,6 +31,32 @@ struct AppearanceSettings: View {
|
||||
updateAppIcon(image: "icon-dark", icon: "DarkAppIcon", tapped: $iconDarkTapped)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Theme", selection: $userInterfaceStyle) {
|
||||
ForEach(interfaceStyles, id: \.self) { style in
|
||||
Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0])
|
||||
}
|
||||
}
|
||||
ColorPicker("Accent color", selection: $uiTintColor, supportsOpacity: false)
|
||||
} header: {
|
||||
Text("Colors")
|
||||
} footer: {
|
||||
Button {
|
||||
uiTintColor = defaultAccentColor
|
||||
setUIAccentColorDefault(defaultAccentColor)
|
||||
} label: {
|
||||
Text("Reset colors").font(.callout)
|
||||
}
|
||||
}
|
||||
.onChange(of: userInterfaceStyle) { _ in
|
||||
sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle
|
||||
setUserInterfaceStyleDefault(userInterfaceStyle)
|
||||
}
|
||||
.onChange(of: uiTintColor) { _ in
|
||||
sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor)
|
||||
setUIAccentColorDefault(uiTintColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +79,44 @@ struct AppearanceSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
func getUIAccentColorDefault() -> CGColor {
|
||||
let defs = UserDefaults.standard
|
||||
return CGColor(
|
||||
red: defs.double(forKey: DEFAULT_ACCENT_COLOR_RED),
|
||||
green: defs.double(forKey: DEFAULT_ACCENT_COLOR_GREEN),
|
||||
blue: defs.double(forKey: DEFAULT_ACCENT_COLOR_BLUE),
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
|
||||
func setUIAccentColorDefault(_ color: CGColor) {
|
||||
if let cs = color.components {
|
||||
let defs = UserDefaults.standard
|
||||
defs.set(cs[0], forKey: DEFAULT_ACCENT_COLOR_RED)
|
||||
defs.set(cs[1], forKey: DEFAULT_ACCENT_COLOR_GREEN)
|
||||
defs.set(cs[2], forKey: DEFAULT_ACCENT_COLOR_BLUE)
|
||||
}
|
||||
}
|
||||
|
||||
func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle {
|
||||
switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) {
|
||||
case 1: return .light
|
||||
case 2: return .dark
|
||||
default: return .unspecified
|
||||
}
|
||||
}
|
||||
|
||||
func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) {
|
||||
var v: Int
|
||||
switch style {
|
||||
case .unspecified: v = 0
|
||||
case .light: v = 1
|
||||
case .dark: v = 2
|
||||
default: v = 0
|
||||
}
|
||||
UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE)
|
||||
}
|
||||
|
||||
struct AppearanceSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AppearanceSettings()
|
||||
|
||||
@@ -14,8 +14,16 @@ struct CallSettings: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
Section("Settings") {
|
||||
Section {
|
||||
Toggle("Connect via relay", isOn: $webrtcPolicyRelay)
|
||||
} header: {
|
||||
Text("Settings")
|
||||
} footer: {
|
||||
if webrtcPolicyRelay {
|
||||
Text("Relay server protects your IP address, but it can observe the duration of the call.")
|
||||
} else {
|
||||
Text("Relay server is only used if necessary. Another party can observe your IP address.")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Limitations") {
|
||||
@@ -31,12 +39,12 @@ struct CallSettings: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func textListItem(_ n: String, _ text: LocalizedStringKey) -> some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
Text(n)
|
||||
Text(text).frame(maxWidth: .infinity, alignment: .leading).padding(.leading, 20)
|
||||
}
|
||||
func textListItem(_ n: String, _ text: LocalizedStringKey) -> some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
Text(n)
|
||||
Text(text).frame(maxWidth: .infinity, alignment: .leading).padding(.leading, 20)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// IncognitoHelp.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by JRoberts on 22.08.2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IncognitoHelp: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Incognito mode")
|
||||
.font(.largeTitle)
|
||||
.padding(.vertical)
|
||||
ScrollView {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct IncognitoHelp_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IncognitoHelp()
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ struct NetworkAndServers: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
Section("") {
|
||||
Section {
|
||||
NavigationLink {
|
||||
SMPServers()
|
||||
.navigationTitle("Your SMP servers")
|
||||
@@ -52,6 +52,10 @@ struct NetworkAndServers: View {
|
||||
settingsRow("app.connected.to.app.below.fill") { Text("Advanced network settings") }
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
Text("Using .onion hosts requires compatible VPN provider.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
||||
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
||||
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
|
||||
let DEFAULT_DEVELOPER_TOOLS = "developerTools"
|
||||
let DEFAULT_ACCENT_COLOR_RED = "accentColorRed"
|
||||
let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen"
|
||||
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue"
|
||||
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle"
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
DEFAULT_SHOW_LA_NOTICE: false,
|
||||
@@ -36,7 +40,11 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
|
||||
DEFAULT_EXPERIMENTAL_CALLS: false,
|
||||
DEFAULT_CHAT_V3_DB_MIGRATION: "offer",
|
||||
DEFAULT_DEVELOPER_TOOLS: false
|
||||
DEFAULT_DEVELOPER_TOOLS: false,
|
||||
DEFAULT_ACCENT_COLOR_RED: 0.000,
|
||||
DEFAULT_ACCENT_COLOR_GREEN: 0.533,
|
||||
DEFAULT_ACCENT_COLOR_BLUE: 1.000,
|
||||
DEFAULT_USER_INTERFACE_STYLE: 0
|
||||
]
|
||||
|
||||
private var indent: CGFloat = 36
|
||||
@@ -52,6 +60,7 @@ struct SettingsView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var showSettings: Bool
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var settingsSheet: SettingsSheet?
|
||||
|
||||
var body: some View {
|
||||
let user: User = chatModel.currentUser!
|
||||
@@ -68,6 +77,9 @@ struct SettingsView: View {
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
incognitoRow()
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
UserAddress()
|
||||
.navigationTitle("Your chat address")
|
||||
@@ -196,9 +208,49 @@ struct SettingsView: View {
|
||||
}
|
||||
.navigationTitle("Your settings")
|
||||
}
|
||||
.sheet(item: $settingsSheet) { sheet in
|
||||
switch sheet {
|
||||
case .incognitoInfo: IncognitoHelp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationAlert {
|
||||
@ViewBuilder private func incognitoRow() -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Image(systemName: chatModel.incognito ? "theatermasks.fill" : "theatermasks")
|
||||
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
|
||||
.foregroundColor(chatModel.incognito ? Color.indigo : .secondary)
|
||||
Toggle(isOn: $chatModel.incognito) {
|
||||
HStack {
|
||||
Text("Incognito")
|
||||
Spacer().frame(width: 4)
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.onTapGesture {
|
||||
settingsSheet = .incognitoInfo
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.incognito) { incognito in
|
||||
incognitoGroupDefault.set(incognito)
|
||||
do {
|
||||
try apiSetIncognito(incognito: incognito)
|
||||
} catch {
|
||||
logger.error("apiSetIncognito: cannot set incognito \(responseError(error))")
|
||||
}
|
||||
}
|
||||
.padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
|
||||
private enum SettingsSheet: Identifiable {
|
||||
case incognitoInfo
|
||||
|
||||
var id: SettingsSheet { get { self } }
|
||||
}
|
||||
|
||||
private enum NotificationAlert {
|
||||
case enable
|
||||
case error(LocalizedStringKey, String)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ struct UserProfile: View {
|
||||
profileNameView("Display name:", user.profile.displayName)
|
||||
profileNameView("Full name:", user.profile.fullName)
|
||||
Button("Edit") {
|
||||
profile = user.profile
|
||||
profile = fromLocalProfile(user.profile)
|
||||
editProfile = true
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ struct UserProfile: View {
|
||||
}
|
||||
|
||||
func startEditingImage(_ user: User) {
|
||||
profile = user.profile
|
||||
profile = fromLocalProfile(user.profile)
|
||||
editProfile = true
|
||||
showChooseSource = true
|
||||
}
|
||||
@@ -141,7 +141,9 @@ struct UserProfile: View {
|
||||
do {
|
||||
if let newProfile = try await apiUpdateProfile(profile: profile) {
|
||||
DispatchQueue.main.async {
|
||||
chatModel.currentUser?.profile = newProfile
|
||||
if let profileId = chatModel.currentUser?.profile.profileId {
|
||||
chatModel.currentUser?.profile = toLocalProfile(profileId, newProfile, "")
|
||||
}
|
||||
profile = newProfile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
<source>
|
||||
</source>
|
||||
<target>
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
<source> </source>
|
||||
<target> </target>
|
||||
@@ -165,6 +172,16 @@
|
||||
<target>A new contact</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve">
|
||||
<source>A random profile will be sent to the contact that you received this link from</source>
|
||||
<target>A random profile will be sent to the contact that you received this link from</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="A random profile will be sent to your contact" xml:space="preserve">
|
||||
<source>A random profile will be sent to your contact</source>
|
||||
<target>A random profile will be sent to your contact</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="About SimpleX" xml:space="preserve">
|
||||
<source>About SimpleX</source>
|
||||
<target>About SimpleX</target>
|
||||
@@ -175,6 +192,11 @@
|
||||
<target>About SimpleX Chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accent color" xml:space="preserve">
|
||||
<source>Accent color</source>
|
||||
<target>Accent color</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept" xml:space="preserve">
|
||||
<source>Accept</source>
|
||||
<target>Accept</target>
|
||||
@@ -191,6 +213,11 @@
|
||||
<target>Accept contact request from %@?</target>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept incognito" xml:space="preserve">
|
||||
<source>Accept incognito</source>
|
||||
<target>Accept incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add contact to start a new chat" xml:space="preserve">
|
||||
<source>Add contact to start a new chat</source>
|
||||
<target>Add contact to start a new chat</target>
|
||||
@@ -261,6 +288,16 @@
|
||||
<target>Call already ended!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contact!" xml:space="preserve">
|
||||
<source>Can't invite contact!</source>
|
||||
<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>
|
||||
@@ -309,7 +346,7 @@
|
||||
<trans-unit id="Chats" xml:space="preserve">
|
||||
<source>Chats</source>
|
||||
<target>Chats</target>
|
||||
<note>back button to return to chats list</note>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
@@ -336,6 +373,11 @@
|
||||
<target>Clear conversation?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Colors" xml:space="preserve">
|
||||
<source>Colors</source>
|
||||
<target>Colors</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Configure SMP servers" xml:space="preserve">
|
||||
<source>Configure SMP servers</source>
|
||||
<target>Configure SMP servers</target>
|
||||
@@ -449,7 +491,7 @@
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Copy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create" xml:space="preserve">
|
||||
<source>Create</source>
|
||||
@@ -514,7 +556,7 @@
|
||||
<trans-unit id="Delete" xml:space="preserve">
|
||||
<source>Delete</source>
|
||||
<target>Delete</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete Contact" xml:space="preserve">
|
||||
<source>Delete Contact</source>
|
||||
@@ -659,7 +701,7 @@
|
||||
<trans-unit id="Edit" xml:space="preserve">
|
||||
<source>Edit</source>
|
||||
<target>Edit</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit group profile" xml:space="preserve">
|
||||
<source>Edit group profile</source>
|
||||
@@ -701,6 +743,11 @@
|
||||
<target>Error accessing database file</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error adding member(s)" xml:space="preserve">
|
||||
<source>Error adding member(s)</source>
|
||||
<target>Error adding member(s)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating group" xml:space="preserve">
|
||||
<source>Error creating group</source>
|
||||
<target>Error creating group</target>
|
||||
@@ -961,6 +1008,26 @@
|
||||
<target>In person or via a video call – the most secure way to connect.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito" xml:space="preserve">
|
||||
<source>Incognito</source>
|
||||
<target>Incognito</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 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">
|
||||
<source>Incoming audio call</source>
|
||||
<target>Incoming audio call</target>
|
||||
@@ -1006,6 +1073,11 @@
|
||||
<target>Invite to group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
<source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source>
|
||||
<target>It allows having many anonymous connections without any shared data between them in a single chat profile.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It can happen when: 1. The messages expire on the server if they were not received for 30 days, 2. The server you use to receive the messages from this contact was updated and restarted. 3. The connection is compromised. Please connect to the developers via Settings to receive the updates about the servers. We will be adding server redundancy to prevent lost messages." xml:space="preserve">
|
||||
<source>It can happen when:
|
||||
1. The messages expire on the server if they were not received for 30 days,
|
||||
@@ -1041,6 +1113,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Join group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Join incognito" xml:space="preserve">
|
||||
<source>Join incognito</source>
|
||||
<target>Join incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Joining group" xml:space="preserve">
|
||||
<source>Joining group</source>
|
||||
<target>Joining group</target>
|
||||
@@ -1091,6 +1168,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mark read" xml:space="preserve">
|
||||
<source>Mark read</source>
|
||||
<target>Mark read</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Markdown in messages" xml:space="preserve">
|
||||
<source>Markdown in messages</source>
|
||||
<target>Markdown in messages</target>
|
||||
@@ -1141,6 +1223,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Most likely this contact has deleted the connection with you.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute" xml:space="preserve">
|
||||
<source>Mute</source>
|
||||
<target>Mute</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network & servers" xml:space="preserve">
|
||||
<source>Network & servers</source>
|
||||
<target>Network & servers</target>
|
||||
@@ -1181,6 +1268,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>New message</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No" xml:space="preserve">
|
||||
<source>No</source>
|
||||
<target>No</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No contacts selected" xml:space="preserve">
|
||||
<source>No contacts selected</source>
|
||||
<target>No contacts selected</target>
|
||||
@@ -1236,6 +1328,21 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>One-time invitation link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve">
|
||||
<source>Onion hosts will be required for connection. Requires enabling VPN.</source>
|
||||
<target>Onion hosts will be required for connection. Requires enabling VPN.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve">
|
||||
<source>Onion hosts will be used when available. Requires enabling VPN.</source>
|
||||
<target>Onion hosts will be used when available. Requires enabling VPN.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Onion hosts will not be used." xml:space="preserve">
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<target>Onion hosts will not be used.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<target>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</target>
|
||||
@@ -1376,6 +1483,16 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Reject contact request</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve">
|
||||
<source>Relay server is only used if necessary. Another party can observe your IP address.</source>
|
||||
<target>Relay server is only used if necessary. Another party can observe your IP address.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve">
|
||||
<source>Relay server protects your IP address, but it can observe the duration of the call.</source>
|
||||
<target>Relay server protects your IP address, but it can observe the duration of the call.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Remove" xml:space="preserve">
|
||||
<source>Remove</source>
|
||||
<target>Remove</target>
|
||||
@@ -1394,6 +1511,16 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>Reply</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Required" xml:space="preserve">
|
||||
<source>Required</source>
|
||||
<target>Required</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reset colors" xml:space="preserve">
|
||||
<source>Reset colors</source>
|
||||
<target>Reset colors</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reset to defaults" xml:space="preserve">
|
||||
@@ -1434,7 +1561,7 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<trans-unit id="Save" xml:space="preserve">
|
||||
<source>Save</source>
|
||||
<target>Save</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
|
||||
<source>Save (and notify contacts)</source>
|
||||
@@ -1466,6 +1593,16 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Scan contact's QR code</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Search" xml:space="preserve">
|
||||
<source>Search</source>
|
||||
<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>
|
||||
@@ -1496,6 +1633,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Servers</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set contact name…" xml:space="preserve">
|
||||
<source>Set contact name…</source>
|
||||
<target>Set contact name…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve">
|
||||
<source>Set timeouts for proxy/VPN</source>
|
||||
<target>Set timeouts for proxy/VPN</target>
|
||||
@@ -1509,7 +1651,7 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<trans-unit id="Share" xml:space="preserve">
|
||||
<source>Share</source>
|
||||
<target>Share</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share invitation link" xml:space="preserve">
|
||||
<source>Share invitation link</source>
|
||||
@@ -1621,6 +1763,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Tap to join</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Tap to join incognito" xml:space="preserve">
|
||||
<source>Tap to join incognito</source>
|
||||
<target>Tap to join incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve">
|
||||
<source>Thank you for installing SimpleX Chat!</source>
|
||||
<target>Thank you for installing SimpleX Chat!</target>
|
||||
@@ -1701,6 +1848,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>To ask any questions and to receive updates:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve">
|
||||
<source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source>
|
||||
<target>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To make a new connection" xml:space="preserve">
|
||||
<source>To make a new connection</source>
|
||||
<target>To make a new connection</target>
|
||||
@@ -1780,6 +1932,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Unlock</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unmute" xml:space="preserve">
|
||||
<source>Unmute</source>
|
||||
<target>Unmute</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update .onion hosts setting?" xml:space="preserve">
|
||||
<source>Update .onion hosts setting?</source>
|
||||
<target>Update .onion hosts setting?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update network settings?" xml:space="preserve">
|
||||
<source>Update network settings?</source>
|
||||
<target>Update network settings?</target>
|
||||
@@ -1790,6 +1952,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Updating settings will re-connect the client to all servers.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve">
|
||||
<source>Updating this setting will re-connect the client to all servers.</source>
|
||||
<target>Updating this setting will re-connect the client to all servers.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use .onion hosts" xml:space="preserve">
|
||||
<source>Use .onion hosts</source>
|
||||
<target>Use .onion hosts</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
|
||||
<source>Use SimpleX Chat servers?</source>
|
||||
<target>Use SimpleX Chat servers?</target>
|
||||
@@ -1800,11 +1972,21 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Use chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve">
|
||||
<source>Using .onion hosts requires compatible VPN provider.</source>
|
||||
<target>Using .onion hosts requires compatible VPN provider.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
|
||||
<source>Using SimpleX Chat servers.</source>
|
||||
<target>Using SimpleX Chat servers.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Video call" xml:space="preserve">
|
||||
<source>Video call</source>
|
||||
<target>Video call</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Waiting for file" xml:space="preserve">
|
||||
<source>Waiting for file</source>
|
||||
<target>Waiting for file</target>
|
||||
@@ -1820,6 +2002,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Welcome %@!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When available" xml:space="preserve">
|
||||
<source>When available</source>
|
||||
<target>When available</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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">
|
||||
<source>You</source>
|
||||
<target>You</target>
|
||||
@@ -1845,9 +2037,9 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You are invited to group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button" xml:space="preserve">
|
||||
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button</source>
|
||||
<target>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button</target>
|
||||
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
|
||||
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
|
||||
<target>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can now send messages to %@" xml:space="preserve">
|
||||
@@ -1935,6 +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'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>
|
||||
@@ -1965,6 +2167,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Your chat profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</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>Your chat profile will be sent to group members</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">
|
||||
<source>Your chat profile will be sent to your contact</source>
|
||||
<target>Your chat profile will be sent to your contact</target>
|
||||
@@ -1975,9 +2182,9 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Your chats</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact can scan it from the app" xml:space="preserve">
|
||||
<source>Your contact can scan it from the app</source>
|
||||
<target>Your contact can scan it from the app</target>
|
||||
<trans-unit id="Your contact can scan it from the app." xml:space="preserve">
|
||||
<source>Your contact can scan it from the app.</source>
|
||||
<target>Your contact can scan it from the app.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve">
|
||||
@@ -2019,6 +2226,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>Your profile, contacts and delivered messages are stored on your device.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your random profile" xml:space="preserve">
|
||||
<source>Your random profile</source>
|
||||
<target>Your random profile</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your settings" xml:space="preserve">
|
||||
<source>Your settings</source>
|
||||
<target>Your settings</target>
|
||||
@@ -2219,6 +2431,16 @@ 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 via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>incognito via contact address link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via one-time link" xml:space="preserve">
|
||||
<source>incognito via one-time link</source>
|
||||
<target>incognito via one-time link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="indirect (%d)" xml:space="preserve">
|
||||
<source>indirect (%d)</source>
|
||||
<target>indirect (%d)</target>
|
||||
@@ -2249,6 +2471,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>italic</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>left</target>
|
||||
@@ -2424,6 +2651,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>you shared one-time link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you shared one-time link incognito" xml:space="preserve">
|
||||
<source>you shared one-time link incognito</source>
|
||||
<target>you shared one-time link incognito</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you: " xml:space="preserve">
|
||||
<source>you: </source>
|
||||
<target>you: </target>
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="13.3" build-num="13E113"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
<source>
|
||||
</source>
|
||||
<target>
|
||||
</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
<source> </source>
|
||||
<target> </target>
|
||||
@@ -165,6 +172,16 @@
|
||||
<target>Новый контакт</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve">
|
||||
<source>A random profile will be sent to the contact that you received this link from</source>
|
||||
<target>Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="A random profile will be sent to your contact" xml:space="preserve">
|
||||
<source>A random profile will be sent to your contact</source>
|
||||
<target>Вашему контакту будет отправлен случайный профиль</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="About SimpleX" xml:space="preserve">
|
||||
<source>About SimpleX</source>
|
||||
<target>О SimpleX</target>
|
||||
@@ -175,6 +192,11 @@
|
||||
<target>Информация о SimpleX Chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accent color" xml:space="preserve">
|
||||
<source>Accent color</source>
|
||||
<target>Основной цвет</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept" xml:space="preserve">
|
||||
<source>Accept</source>
|
||||
<target>Принять</target>
|
||||
@@ -191,6 +213,11 @@
|
||||
<target>Принять запрос на соединение от %@?</target>
|
||||
<note>notification body</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept incognito" xml:space="preserve">
|
||||
<source>Accept incognito</source>
|
||||
<target>Принять инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add contact to start a new chat" xml:space="preserve">
|
||||
<source>Add contact to start a new chat</source>
|
||||
<target>Добавьте контакт, чтобы начать разговор</target>
|
||||
@@ -261,6 +288,16 @@
|
||||
<target>Звонок уже завершен!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contact!" xml:space="preserve">
|
||||
<source>Can't invite contact!</source>
|
||||
<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>
|
||||
@@ -309,7 +346,7 @@
|
||||
<trans-unit id="Chats" xml:space="preserve">
|
||||
<source>Chats</source>
|
||||
<target>Чаты</target>
|
||||
<note>back button to return to chats list</note>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
@@ -336,6 +373,11 @@
|
||||
<target>Очистить разговор?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Colors" xml:space="preserve">
|
||||
<source>Colors</source>
|
||||
<target>Цвета</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Configure SMP servers" xml:space="preserve">
|
||||
<source>Configure SMP servers</source>
|
||||
<target>Настройка SMP серверов</target>
|
||||
@@ -449,7 +491,7 @@
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Скопировать</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create" xml:space="preserve">
|
||||
<source>Create</source>
|
||||
@@ -514,7 +556,7 @@
|
||||
<trans-unit id="Delete" xml:space="preserve">
|
||||
<source>Delete</source>
|
||||
<target>Удалить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete Contact" xml:space="preserve">
|
||||
<source>Delete Contact</source>
|
||||
@@ -659,7 +701,7 @@
|
||||
<trans-unit id="Edit" xml:space="preserve">
|
||||
<source>Edit</source>
|
||||
<target>Редактировать</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit group profile" xml:space="preserve">
|
||||
<source>Edit group profile</source>
|
||||
@@ -701,6 +743,11 @@
|
||||
<target>Ошибка при доступе к данным чата</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error adding member(s)" xml:space="preserve">
|
||||
<source>Error adding member(s)</source>
|
||||
<target>Ошибка при добавлении членов группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating group" xml:space="preserve">
|
||||
<source>Error creating group</source>
|
||||
<target>Ошибка при создании группы</target>
|
||||
@@ -961,6 +1008,26 @@
|
||||
<target>При встрече или в видеозвонке – самый безопасный способ установить соединение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Incognito" xml:space="preserve">
|
||||
<source>Incognito</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 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">
|
||||
<source>Incoming audio call</source>
|
||||
<target>Входящий аудиозвонок</target>
|
||||
@@ -1006,6 +1073,11 @@
|
||||
<target>Пригласить в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
<source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source>
|
||||
<target>Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It can happen when: 1. The messages expire on the server if they were not received for 30 days, 2. The server you use to receive the messages from this contact was updated and restarted. 3. The connection is compromised. Please connect to the developers via Settings to receive the updates about the servers. We will be adding server redundancy to prevent lost messages." xml:space="preserve">
|
||||
<source>It can happen when:
|
||||
1. The messages expire on the server if they were not received for 30 days,
|
||||
@@ -1041,6 +1113,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="Join incognito" xml:space="preserve">
|
||||
<source>Join incognito</source>
|
||||
<target>Вступить инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Joining group" xml:space="preserve">
|
||||
<source>Joining group</source>
|
||||
<target>Вступление в группу</target>
|
||||
@@ -1091,6 +1168,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?*</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mark read" xml:space="preserve">
|
||||
<source>Mark read</source>
|
||||
<target>Прочитано</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Markdown in messages" xml:space="preserve">
|
||||
<source>Markdown in messages</source>
|
||||
<target>Форматирование сообщений</target>
|
||||
@@ -1141,6 +1223,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="Mute" xml:space="preserve">
|
||||
<source>Mute</source>
|
||||
<target>Без звука</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network & servers" xml:space="preserve">
|
||||
<source>Network & servers</source>
|
||||
<target>Сеть & серверы</target>
|
||||
@@ -1181,6 +1268,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Новое сообщение</target>
|
||||
<note>notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No" xml:space="preserve">
|
||||
<source>No</source>
|
||||
<target>Нет</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No contacts selected" xml:space="preserve">
|
||||
<source>No contacts selected</source>
|
||||
<target>Контакты не выбраны</target>
|
||||
@@ -1236,6 +1328,21 @@ 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="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve">
|
||||
<source>Onion hosts will be required for connection. Requires enabling VPN.</source>
|
||||
<target>Подключаться только к onion хостам. Требуется включенный VPN.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve">
|
||||
<source>Onion hosts will be used when available. Requires enabling VPN.</source>
|
||||
<target>Onion хосты используются, если возможно. Требуется включенный VPN.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Onion hosts will not be used." xml:space="preserve">
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<target>Onion хосты не используются.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<target>Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**</target>
|
||||
@@ -1376,6 +1483,16 @@ 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="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve">
|
||||
<source>Relay server is only used if necessary. Another party can observe your IP address.</source>
|
||||
<target>Relay сервер используется только при необходимости. Другая сторона может видеть ваш IP адрес.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve">
|
||||
<source>Relay server protects your IP address, but it can observe the duration of the call.</source>
|
||||
<target>Relay сервер защищает ваш IP адрес, но может отслеживать продолжительность звонка.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Remove" xml:space="preserve">
|
||||
<source>Remove</source>
|
||||
<target>Удалить</target>
|
||||
@@ -1394,6 +1511,16 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>Ответить</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Required" xml:space="preserve">
|
||||
<source>Required</source>
|
||||
<target>Обязательно</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reset colors" xml:space="preserve">
|
||||
<source>Reset colors</source>
|
||||
<target>Сбросить цвета</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reset to defaults" xml:space="preserve">
|
||||
@@ -1434,7 +1561,7 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<trans-unit id="Save" xml:space="preserve">
|
||||
<source>Save</source>
|
||||
<target>Сохранить</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
|
||||
<source>Save (and notify contacts)</source>
|
||||
@@ -1466,6 +1593,16 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Сосканировать QR код контакта</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Search" xml:space="preserve">
|
||||
<source>Search</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>
|
||||
@@ -1496,6 +1633,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="Set contact name…" xml:space="preserve">
|
||||
<source>Set contact name…</source>
|
||||
<target>Имя контакта…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve">
|
||||
<source>Set timeouts for proxy/VPN</source>
|
||||
<target>Установить таймауты для прокси/VPN</target>
|
||||
@@ -1509,7 +1651,7 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<trans-unit id="Share" xml:space="preserve">
|
||||
<source>Share</source>
|
||||
<target>Поделиться</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share invitation link" xml:space="preserve">
|
||||
<source>Share invitation link</source>
|
||||
@@ -1621,6 +1763,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="Tap to join incognito" xml:space="preserve">
|
||||
<source>Tap to join incognito</source>
|
||||
<target>Нажмите, чтобы вступить инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve">
|
||||
<source>Thank you for installing SimpleX Chat!</source>
|
||||
<target>Спасибо, что установили SimpleX Chat!</target>
|
||||
@@ -1701,6 +1848,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="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve">
|
||||
<source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source>
|
||||
<target>Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To make a new connection" xml:space="preserve">
|
||||
<source>To make a new connection</source>
|
||||
<target>Чтобы соединиться</target>
|
||||
@@ -1780,6 +1932,16 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Разблокировать</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unmute" xml:space="preserve">
|
||||
<source>Unmute</source>
|
||||
<target>Уведомлять</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update .onion hosts setting?" xml:space="preserve">
|
||||
<source>Update .onion hosts setting?</source>
|
||||
<target>Обновить настройки .onion хостов?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update network settings?" xml:space="preserve">
|
||||
<source>Update network settings?</source>
|
||||
<target>Обновить настройки сети?</target>
|
||||
@@ -1787,7 +1949,17 @@ To connect, please ask your contact to create another connection link and check
|
||||
</trans-unit>
|
||||
<trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve">
|
||||
<source>Updating settings will re-connect the client to all servers.</source>
|
||||
<target>Обновление настроек приведет к переподключению клиента ко всем серверам.</target>
|
||||
<target>Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve">
|
||||
<source>Updating this setting will re-connect the client to all servers.</source>
|
||||
<target>Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use .onion hosts" xml:space="preserve">
|
||||
<source>Use .onion hosts</source>
|
||||
<target>Использовать .onion хосты</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
|
||||
@@ -1800,11 +1972,21 @@ 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="Using .onion hosts requires compatible VPN provider." xml:space="preserve">
|
||||
<source>Using .onion hosts requires compatible VPN provider.</source>
|
||||
<target>Для использования .onion хостов требуется совместимый VPN провайдер.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
|
||||
<source>Using SimpleX Chat servers.</source>
|
||||
<target>Используются серверы, предоставленные SimpleX Chat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Video call" xml:space="preserve">
|
||||
<source>Video call</source>
|
||||
<target>Видеозвонок</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Waiting for file" xml:space="preserve">
|
||||
<source>Waiting for file</source>
|
||||
<target>Ожидается прием файла</target>
|
||||
@@ -1820,6 +2002,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="When available" xml:space="preserve">
|
||||
<source>When available</source>
|
||||
<target>Когда возможно</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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">
|
||||
<source>You</source>
|
||||
<target>Вы</target>
|
||||
@@ -1845,8 +2037,8 @@ 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 can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button" xml:space="preserve">
|
||||
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button</source>
|
||||
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
|
||||
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
|
||||
<target>Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1935,6 +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'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>
|
||||
@@ -1965,6 +2167,11 @@ 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="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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve">
|
||||
<source>Your chat profile will be sent to your contact</source>
|
||||
<target>Ваш профиль будет отправлен вашему контакту</target>
|
||||
@@ -1975,9 +2182,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="Your contact can scan it from the app" xml:space="preserve">
|
||||
<source>Your contact can scan it from the app</source>
|
||||
<target>Ваш контакт может сосканировать QR в приложении</target>
|
||||
<trans-unit id="Your contact can scan it from the app." xml:space="preserve">
|
||||
<source>Your contact can scan it from the app.</source>
|
||||
<target>Ваш контакт может сосканировать QR код в приложении</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve">
|
||||
@@ -2019,6 +2226,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>Ваш профиль, контакты и доставленные сообщения хранятся на вашем устройстве.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your random profile" xml:space="preserve">
|
||||
<source>Your random profile</source>
|
||||
<target>Ваш случайный профиль</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your settings" xml:space="preserve">
|
||||
<source>Your settings</source>
|
||||
<target>Настройки</target>
|
||||
@@ -2219,6 +2431,16 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>профиль группы обновлен</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>инкогнито через ссылку-контакт</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via one-time link" xml:space="preserve">
|
||||
<source>incognito via one-time link</source>
|
||||
<target>инкогнито через одноразовую ссылку</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="indirect (%d)" xml:space="preserve">
|
||||
<source>indirect (%d)</source>
|
||||
<target>непрямое (%d)</target>
|
||||
@@ -2249,6 +2471,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>курсив</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<target>покинул(а) группу</target>
|
||||
@@ -2424,6 +2651,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>вы создали ссылку</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you shared one-time link incognito" xml:space="preserve">
|
||||
<source>you shared one-time link incognito</source>
|
||||
<target>вы создали ссылку инкогнито</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you: " xml:space="preserve">
|
||||
<source>you: </source>
|
||||
<target>вы: </target>
|
||||
|
||||
@@ -160,6 +160,7 @@ func startChat() -> User? {
|
||||
let justStarted = try apiStartChat()
|
||||
if justStarted {
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try apiSetIncognito(incognito: incognitoGroupDefault.get())
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
Task { await receiveMessages() }
|
||||
}
|
||||
@@ -248,6 +249,12 @@ func apiSetFilesFolder(filesFolder: String) throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetIncognito(incognito: Bool) throws {
|
||||
let r = sendSimpleXCmd(.setIncognito(incognito: incognito))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r {
|
||||
|
||||
@@ -13,11 +13,6 @@
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
|
||||
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; };
|
||||
5C00165628B02AF40094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165128B02AF30094D739 /* libffi.a */; };
|
||||
5C00165728B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */; };
|
||||
5C00165828B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */; };
|
||||
5C00165928B02AF40094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165428B02AF30094D739 /* libgmp.a */; };
|
||||
5C00165A28B02AF40094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00165528B02AF30094D739 /* libgmpxx.a */; };
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
|
||||
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
|
||||
@@ -131,6 +126,12 @@
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||
64F1CC4128B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */; };
|
||||
64F1CC4228B3A99A00CD1FB1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */; };
|
||||
64F1CC4328B3A99A00CD1FB1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3E28B3A99900CD1FB1 /* libgmp.a */; };
|
||||
64F1CC4428B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */; };
|
||||
64F1CC4528B3A99A00CD1FB1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64F1CC4028B3A99A00CD1FB1 /* libffi.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -196,11 +197,6 @@
|
||||
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
|
||||
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
|
||||
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
|
||||
5C00165128B02AF30094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a"; sourceTree = "<group>"; };
|
||||
5C00165428B02AF30094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C00165528B02AF30094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
|
||||
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
|
||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
|
||||
@@ -317,6 +313,12 @@
|
||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||
64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a"; sourceTree = "<group>"; };
|
||||
64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64F1CC3E28B3A99900CD1FB1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
64F1CC4028B3A99A00CD1FB1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -349,13 +351,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
64F1CC4428B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a in Frameworks */,
|
||||
64F1CC4228B3A99A00CD1FB1 /* libgmpxx.a in Frameworks */,
|
||||
64F1CC4328B3A99A00CD1FB1 /* libgmp.a in Frameworks */,
|
||||
64F1CC4128B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a in Frameworks */,
|
||||
64F1CC4528B3A99A00CD1FB1 /* libffi.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5C00165728B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a in Frameworks */,
|
||||
5C00165828B02AF40094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a in Frameworks */,
|
||||
5C00165628B02AF40094D739 /* libffi.a in Frameworks */,
|
||||
5C00165928B02AF40094D739 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C00165A28B02AF40094D739 /* libgmpxx.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -410,11 +412,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C00165128B02AF30094D739 /* libffi.a */,
|
||||
5C00165428B02AF30094D739 /* libgmp.a */,
|
||||
5C00165528B02AF30094D739 /* libgmpxx.a */,
|
||||
5C00165228B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J-ghc8.10.7.a */,
|
||||
5C00165328B02AF30094D739 /* libHSsimplex-chat-3.1.0-KA4pfwpgEHbFrTKfOobU7J.a */,
|
||||
64F1CC4028B3A99A00CD1FB1 /* libffi.a */,
|
||||
64F1CC3E28B3A99900CD1FB1 /* libgmp.a */,
|
||||
64F1CC3D28B3A99900CD1FB1 /* libgmpxx.a */,
|
||||
64F1CC3F28B3A99A00CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5-ghc8.10.7.a */,
|
||||
64F1CC3C28B3A99900CD1FB1 /* libHSsimplex-chat-3.2.0-6p2ah0FJ9icAh1HFBZcXP5.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -553,6 +555,7 @@
|
||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
||||
640F50E227CF991C001E05C2 /* SMPServers.swift */,
|
||||
5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */,
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
||||
);
|
||||
path = UserSettings;
|
||||
sourceTree = "<group>";
|
||||
@@ -869,6 +872,7 @@
|
||||
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
|
||||
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */,
|
||||
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */,
|
||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */,
|
||||
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */,
|
||||
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
|
||||
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
|
||||
@@ -1160,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;
|
||||
@@ -1202,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;
|
||||
@@ -1281,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;
|
||||
@@ -1311,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;
|
||||
@@ -1342,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;
|
||||
@@ -1388,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;
|
||||
|
||||
@@ -20,6 +20,7 @@ public enum ChatCommand {
|
||||
case apiActivateChat
|
||||
case apiSuspendChat(timeoutMicroseconds: Int)
|
||||
case setFilesFolder(filesFolder: String)
|
||||
case setIncognito(incognito: Bool)
|
||||
case apiExportArchive(config: ArchiveConfig)
|
||||
case apiImportArchive(config: ArchiveConfig)
|
||||
case apiDeleteStorage
|
||||
@@ -54,6 +55,7 @@ public enum ChatCommand {
|
||||
case apiClearChat(type: ChatType, id: Int64)
|
||||
case listContacts
|
||||
case apiUpdateProfile(profile: Profile)
|
||||
case apiSetContactAlias(contactId: Int64, localAlias: String)
|
||||
case createMyAddress
|
||||
case deleteMyAddress
|
||||
case showMyAddress
|
||||
@@ -82,6 +84,7 @@ public enum ChatCommand {
|
||||
case .apiActivateChat: return "/_app activate"
|
||||
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
||||
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
||||
case let .setIncognito(incognito): return "/incognito \(incognito ? "on" : "off")"
|
||||
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||
case .apiDeleteStorage: return "/_db delete"
|
||||
@@ -118,6 +121,7 @@ public enum ChatCommand {
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||
case .listContacts: return "/contacts"
|
||||
case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))"
|
||||
case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))"
|
||||
case .createMyAddress: return "/address"
|
||||
case .deleteMyAddress: return "/delete_address"
|
||||
case .showMyAddress: return "/show_address"
|
||||
@@ -148,6 +152,7 @@ public enum ChatCommand {
|
||||
case .apiActivateChat: return "apiActivateChat"
|
||||
case .apiSuspendChat: return "apiSuspendChat"
|
||||
case .setFilesFolder: return "setFilesFolder"
|
||||
case .setIncognito: return "setIncognito"
|
||||
case .apiExportArchive: return "apiExportArchive"
|
||||
case .apiImportArchive: return "apiImportArchive"
|
||||
case .apiDeleteStorage: return "apiDeleteStorage"
|
||||
@@ -181,6 +186,7 @@ public enum ChatCommand {
|
||||
case .apiClearChat: return "apiClearChat"
|
||||
case .listContacts: return "listContacts"
|
||||
case .apiUpdateProfile: return "apiUpdateProfile"
|
||||
case .apiSetContactAlias: return "apiSetContactAlias"
|
||||
case .createMyAddress: return "createMyAddress"
|
||||
case .deleteMyAddress: return "deleteMyAddress"
|
||||
case .showMyAddress: return "showMyAddress"
|
||||
@@ -225,7 +231,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case apiChat(chat: ChatData)
|
||||
case userSMPServers(smpServers: [String])
|
||||
case networkConfig(networkConfig: NetCfg)
|
||||
case contactInfo(contact: Contact, connectionStats: ConnectionStats)
|
||||
case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
|
||||
case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
|
||||
case invitation(connReqInvitation: String)
|
||||
case sentConfirmation
|
||||
@@ -235,6 +241,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case chatCleared(chatInfo: ChatInfo)
|
||||
case userProfileNoChange
|
||||
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
case contactAliasUpdated(toContact: Contact)
|
||||
case userContactLink(connReqContact: String)
|
||||
case userContactLinkCreated(connReqContact: String)
|
||||
case userContactLinkDeleted
|
||||
@@ -326,6 +333,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .chatCleared: return "chatCleared"
|
||||
case .userProfileNoChange: return "userProfileNoChange"
|
||||
case .userProfileUpdated: return "userProfileUpdated"
|
||||
case .contactAliasUpdated: return "contactAliasUpdated"
|
||||
case .userContactLink: return "userContactLink"
|
||||
case .userContactLinkCreated: return "userContactLinkCreated"
|
||||
case .userContactLinkDeleted: return "userContactLinkDeleted"
|
||||
@@ -407,8 +415,8 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .apiChat(chat): return String(describing: chat)
|
||||
case let .userSMPServers(smpServers): return String(describing: smpServers)
|
||||
case let .networkConfig(networkConfig): return String(describing: networkConfig)
|
||||
case let .contactInfo(contact, connectionStats): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))"
|
||||
case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\\nconnectionStats_: \(String(describing: connectionStats_))"
|
||||
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_): 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
|
||||
@@ -417,6 +425,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .chatCleared(chatInfo): return String(describing: chatInfo)
|
||||
case .userProfileNoChange: return noDetails
|
||||
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
|
||||
case let .contactAliasUpdated(toContact): return String(describing: toContact)
|
||||
case let .userContactLink(connReq): return connReq
|
||||
case let .userContactLinkCreated(connReq): return connReq
|
||||
case .userContactLinkDeleted: return noDetails
|
||||
@@ -561,7 +570,7 @@ public enum OnionHosts: String, Identifiable {
|
||||
switch self {
|
||||
case .no: return "No"
|
||||
case .prefer: return "When available"
|
||||
case .require: return "Requred"
|
||||
case .require: return "Required"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt"
|
||||
let GROUP_DEFAULT_INCOGNITO = "incognito"
|
||||
|
||||
let APP_GROUP_NAME = "group.chat.simplex.app"
|
||||
|
||||
@@ -37,7 +38,8 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive,
|
||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE: KeepAliveOpts.defaults.keepIdle,
|
||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL: KeepAliveOpts.defaults.keepIntvl,
|
||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt
|
||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt,
|
||||
GROUP_DEFAULT_INCOGNITO: false
|
||||
])
|
||||
}
|
||||
|
||||
@@ -82,6 +84,8 @@ public let ntfPreviewModeGroupDefault = EnumDefault<NotificationPreviewMode>(
|
||||
withDefault: .message
|
||||
)
|
||||
|
||||
public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO)
|
||||
|
||||
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
|
||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||
|
||||
@@ -13,18 +13,19 @@ public struct User: Decodable, NamedChat {
|
||||
var userId: Int64
|
||||
var userContactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
public var profile: Profile
|
||||
public var profile: LocalProfile
|
||||
var activeUser: Bool
|
||||
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
public var localAlias: String { get { "" } }
|
||||
|
||||
public static let sampleData = User(
|
||||
userId: 1,
|
||||
userContactId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: Profile.sampleData,
|
||||
profile: LocalProfile.sampleData,
|
||||
activeUser: true
|
||||
)
|
||||
}
|
||||
@@ -43,6 +44,7 @@ public struct Profile: Codable, NamedChat {
|
||||
public var displayName: String
|
||||
public var fullName: String
|
||||
public var image: String?
|
||||
public var localAlias: String { get { "" } }
|
||||
|
||||
var profileViewName: String {
|
||||
(fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))"
|
||||
@@ -54,6 +56,43 @@ public struct Profile: Codable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct LocalProfile: Codable, NamedChat {
|
||||
public init(profileId: Int64, displayName: String, fullName: String, image: String? = nil, localAlias: String) {
|
||||
self.profileId = profileId
|
||||
self.displayName = displayName
|
||||
self.fullName = fullName
|
||||
self.image = image
|
||||
self.localAlias = localAlias
|
||||
}
|
||||
|
||||
public var profileId: Int64
|
||||
public var displayName: String
|
||||
public var fullName: String
|
||||
public var image: String?
|
||||
public var localAlias: String
|
||||
|
||||
var profileViewName: String {
|
||||
localAlias == ""
|
||||
? (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))"
|
||||
: localAlias
|
||||
}
|
||||
|
||||
static let sampleData = LocalProfile(
|
||||
profileId: 1,
|
||||
displayName: "alice",
|
||||
fullName: "Alice",
|
||||
localAlias: ""
|
||||
)
|
||||
}
|
||||
|
||||
public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias: String) -> LocalProfile {
|
||||
LocalProfile(profileId: profileId, displayName: profile.displayName, fullName: profile.fullName, image: profile.image, localAlias: localAlias)
|
||||
}
|
||||
|
||||
public func fromLocalProfile (_ profile: LocalProfile) -> Profile {
|
||||
Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image)
|
||||
}
|
||||
|
||||
public enum ChatType: String {
|
||||
case direct = "@"
|
||||
case group = "#"
|
||||
@@ -65,11 +104,14 @@ public protocol NamedChat {
|
||||
var displayName: String { get }
|
||||
var fullName: String { get }
|
||||
var image: String? { get }
|
||||
var localAlias: String { get }
|
||||
}
|
||||
|
||||
extension NamedChat {
|
||||
public var chatViewName: String {
|
||||
get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") }
|
||||
localAlias == ""
|
||||
? displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)")
|
||||
: localAlias
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +167,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
public var localAlias: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.localAlias
|
||||
case let .group(groupInfo): return groupInfo.localAlias
|
||||
case let .contactRequest(contactRequest): return contactRequest.localAlias
|
||||
case let .contactConnection(contactConnection): return contactConnection.localAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var id: ChatId {
|
||||
get {
|
||||
switch self {
|
||||
@@ -180,6 +233,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
public var incognito: Bool {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.contactConnIncognito
|
||||
case let .group(groupInfo): return groupInfo.membership.memberIncognito
|
||||
case .contactRequest: return false
|
||||
case let .contactConnection(contactConnection): return contactConnection.incognito
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var contact: Contact? {
|
||||
get {
|
||||
switch self {
|
||||
@@ -247,9 +311,9 @@ public struct ChatStats: Decodable {
|
||||
}
|
||||
|
||||
public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
var contactId: Int64
|
||||
public var contactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
public var profile: Profile
|
||||
public var profile: LocalProfile
|
||||
public var activeConn: Connection
|
||||
public var viaGroup: Int64?
|
||||
public var chatSettings: ChatSettings
|
||||
@@ -260,18 +324,23 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
public var apiId: Int64 { get { contactId } }
|
||||
public var ready: Bool { get { activeConn.connStatus == .ready } }
|
||||
public var sendMsgEnabled: Bool { get { true } }
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
public var localAlias: String { profile.localAlias }
|
||||
|
||||
public func isIndirectContact() -> Bool {
|
||||
return activeConn.connLevel > 0 || viaGroup != nil
|
||||
public var isIndirectContact: Bool {
|
||||
activeConn.connLevel > 0 || viaGroup != nil
|
||||
}
|
||||
|
||||
public var contactConnIncognito: Bool {
|
||||
activeConn.customUserProfileId != nil
|
||||
}
|
||||
|
||||
public static let sampleData = Contact(
|
||||
contactId: 1,
|
||||
localDisplayName: "alice",
|
||||
profile: Profile.sampleData,
|
||||
profile: LocalProfile.sampleData,
|
||||
activeConn: Connection.sampleData,
|
||||
chatSettings: ChatSettings.defaults,
|
||||
createdAt: .now,
|
||||
@@ -295,6 +364,7 @@ public struct Connection: Decodable {
|
||||
var connId: Int64
|
||||
var connStatus: ConnStatus
|
||||
public var connLevel: Int
|
||||
public var customUserProfileId: Int64?
|
||||
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
|
||||
@@ -336,6 +406,7 @@ public struct UserContactRequest: Decodable, NamedChat {
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
public var localAlias: String { "" }
|
||||
|
||||
public static let sampleData = UserContactRequest(
|
||||
contactRequestId: 1,
|
||||
@@ -352,6 +423,7 @@ public struct PendingContactConnection: Decodable, NamedChat {
|
||||
var pccAgentConnId: String
|
||||
var pccConnStatus: ConnStatus
|
||||
public var viaContactUri: Bool
|
||||
public var customUserProfileId: Int64?
|
||||
var createdAt: Date
|
||||
public var updatedAt: Date
|
||||
|
||||
@@ -376,16 +448,37 @@ public struct PendingContactConnection: Decodable, NamedChat {
|
||||
}
|
||||
public var fullName: String { get { "" } }
|
||||
public var image: String? { get { nil } }
|
||||
public var localAlias: String { "" }
|
||||
public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } }
|
||||
|
||||
public var incognito: Bool {
|
||||
customUserProfileId != nil
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
get {
|
||||
if let initiated = pccConnStatus.initiated {
|
||||
return initiated && !viaContactUri
|
||||
? NSLocalizedString("you shared one-time link", comment: "chat list item description")
|
||||
: viaContactUri
|
||||
? NSLocalizedString("via contact address link", comment: "chat list item description")
|
||||
: NSLocalizedString("via one-time link", comment: "chat list item description")
|
||||
var desc: String
|
||||
if initiated && !viaContactUri {
|
||||
if incognito {
|
||||
desc = NSLocalizedString("you shared one-time link incognito", comment: "chat list item description")
|
||||
} else {
|
||||
desc = NSLocalizedString("you shared one-time link", comment: "chat list item description")
|
||||
}
|
||||
} else if viaContactUri {
|
||||
if incognito {
|
||||
desc = NSLocalizedString("incognito via contact address link", comment: "chat list item description")
|
||||
} else {
|
||||
desc = NSLocalizedString("via contact address link", comment: "chat list item description")
|
||||
}
|
||||
} else {
|
||||
if incognito {
|
||||
desc = NSLocalizedString("incognito via one-time link", comment: "chat list item description")
|
||||
} else {
|
||||
desc = NSLocalizedString("via one-time link", comment: "chat list item description")
|
||||
}
|
||||
}
|
||||
return desc
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
@@ -443,6 +536,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
var localDisplayName: GroupName
|
||||
public var groupProfile: GroupProfile
|
||||
public var membership: GroupMember
|
||||
public var hostConnCustomUserProfileId: Int64?
|
||||
public var chatSettings: ChatSettings
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
@@ -454,6 +548,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
public var displayName: String { get { groupProfile.displayName } }
|
||||
public var fullName: String { get { groupProfile.fullName } }
|
||||
public var image: String? { get { groupProfile.image } }
|
||||
public var localAlias: String { "" }
|
||||
|
||||
public var canEdit: Bool {
|
||||
return membership.memberRole == .owner && membership.memberCurrent
|
||||
@@ -472,6 +567,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
localDisplayName: "team",
|
||||
groupProfile: GroupProfile.sampleData,
|
||||
membership: GroupMember.sampleData,
|
||||
hostConnCustomUserProfileId: nil,
|
||||
chatSettings: ChatSettings.defaults,
|
||||
createdAt: .now,
|
||||
updatedAt: .now
|
||||
@@ -488,6 +584,7 @@ public struct GroupProfile: Codable, NamedChat {
|
||||
public var displayName: String
|
||||
public var fullName: String
|
||||
public var image: String?
|
||||
public var localAlias: String { "" }
|
||||
|
||||
public static let sampleData = GroupProfile(
|
||||
displayName: "team",
|
||||
@@ -504,12 +601,18 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
public var memberStatus: GroupMemberStatus
|
||||
public var invitedBy: InvitedBy
|
||||
public var localDisplayName: ContactName
|
||||
public var memberProfile: Profile
|
||||
public var memberProfile: LocalProfile
|
||||
public var memberContactId: Int64?
|
||||
public var memberContactProfileId: Int64
|
||||
public var activeConn: Connection?
|
||||
|
||||
public var id: String { "#\(groupId) @\(groupMemberId)" }
|
||||
public var displayName: String { get { memberProfile.displayName } }
|
||||
public var displayName: String {
|
||||
get {
|
||||
let p = memberProfile
|
||||
return p.localAlias == "" ? p.displayName : p.localAlias
|
||||
}
|
||||
}
|
||||
public var fullName: String { get { memberProfile.fullName } }
|
||||
public var image: String? { get { memberProfile.image } }
|
||||
|
||||
@@ -526,7 +629,9 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
public var chatViewName: String {
|
||||
get {
|
||||
let p = memberProfile
|
||||
return p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)")
|
||||
return p.localAlias == ""
|
||||
? p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)")
|
||||
: p.localAlias
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,6 +673,10 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
&& userRole >= .admin && userRole >= memberRole && membership.memberCurrent
|
||||
}
|
||||
|
||||
public var memberIncognito: Bool {
|
||||
memberProfile.profileId != memberContactProfileId
|
||||
}
|
||||
|
||||
public static let sampleData = GroupMember(
|
||||
groupMemberId: 1,
|
||||
groupId: 1,
|
||||
@@ -577,8 +686,9 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
memberStatus: .memComplete,
|
||||
invitedBy: .user,
|
||||
localDisplayName: "alice",
|
||||
memberProfile: Profile.sampleData,
|
||||
memberProfile: LocalProfile.sampleData,
|
||||
memberContactId: 1,
|
||||
memberContactProfileId: 1,
|
||||
activeConn: Connection.sampleData
|
||||
)
|
||||
}
|
||||
@@ -783,7 +893,7 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
public var memberDisplayName: String? {
|
||||
get {
|
||||
if case let .groupRcv(groupMember) = chatDir {
|
||||
return groupMember.memberProfile.displayName
|
||||
return groupMember.displayName
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"\n" = "\n";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
" " = " ";
|
||||
|
||||
@@ -106,6 +109,12 @@
|
||||
/* notification title */
|
||||
"A new contact" = "Новый контакт";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"A random profile will be sent to the contact that you received this link from" = "Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"A random profile will be sent to your contact" = "Вашему контакту будет отправлен случайный профиль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"About SimpleX" = "О SimpleX";
|
||||
|
||||
@@ -115,6 +124,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"above, then choose:" = "наверху, затем выберите:";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Accent color" = "Основной цвет";
|
||||
|
||||
/* accept contact request via notification
|
||||
accept incoming call via notification */
|
||||
"Accept" = "Принять";
|
||||
@@ -125,6 +137,9 @@
|
||||
/* notification body */
|
||||
"Accept contact request from %@?" = "Принять запрос на соединение от %@?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Accept incognito" = "Принять инкогнито";
|
||||
|
||||
/* call status */
|
||||
"accepted call" = " принятый звонок";
|
||||
|
||||
@@ -194,6 +209,12 @@
|
||||
/* call status */
|
||||
"calling…" = "входящий звонок…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contact!" = "Нельзя пригласить контакт!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contacts!" = "Нельзя пригласить контакты!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Cancel" = "Отменить";
|
||||
|
||||
@@ -221,7 +242,7 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Chat with the developers" = "Соединиться с разработчиками";
|
||||
|
||||
/* back button to return to chats list */
|
||||
/* No comment provided by engineer. */
|
||||
"Chats" = "Чаты";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -242,6 +263,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"colored" = "цвет";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Colors" = "Цвета";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"complete" = "соединение завершено";
|
||||
|
||||
@@ -350,7 +374,7 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Contact name" = "Имена контактов";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
/* chat item action */
|
||||
"Copy" = "Скопировать";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -392,7 +416,7 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Decentralized" = "Децентрализованный";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
/* chat item action */
|
||||
"Delete" = "Удалить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -494,7 +518,7 @@
|
||||
/* No comment provided by engineer. */
|
||||
"e2e encrypted" = "e2e зашифровано";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
/* chat item action */
|
||||
"Edit" = "Редактировать";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -530,6 +554,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error accessing database file" = "Ошибка при доступе к данным чата";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error adding member(s)" = "Ошибка при добавлении членов группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error creating group" = "Ошибка при создании группы";
|
||||
|
||||
@@ -692,6 +719,24 @@
|
||||
/* No comment provided by engineer. */
|
||||
"In person or via a video call – the most secure way to connect." = "При встрече или в видеозвонке – самый безопасный способ установить соединение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incognito" = "Инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Incognito mode" = "Режим Инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"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" = "инкогнито через ссылку-контакт";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via one-time link" = "инкогнито через одноразовую ссылку";
|
||||
|
||||
/* notification */
|
||||
"Incoming audio call" = "Входящий аудиозвонок";
|
||||
|
||||
@@ -734,6 +779,9 @@
|
||||
/* chat list item title */
|
||||
"invited to connect" = "приглашение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя.";
|
||||
|
||||
/* 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Мы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения.";
|
||||
|
||||
@@ -749,9 +797,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Join" = "Вступить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"join as %@" = "вступить как %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Join group" = "Вступить в группу";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Join incognito" = "Вступить инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Joining group" = "Вступление в группу";
|
||||
|
||||
@@ -785,6 +839,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?*";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Mark read" = "Прочитано";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Markdown in messages" = "Форматирование сообщений";
|
||||
|
||||
@@ -827,6 +884,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Most likely this contact has deleted the connection with you." = "Скорее всего, этот контакт удалил соединение с вами.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Mute" = "Без звука";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Network & servers" = "Сеть & серверы";
|
||||
|
||||
@@ -854,6 +914,9 @@
|
||||
/* notification */
|
||||
"New message" = "Новое сообщение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No" = "Нет";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No contacts selected" = "Контакты не выбраны";
|
||||
|
||||
@@ -890,6 +953,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
"One-time invitation link" = "Одноразовая ссылка";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Onion hosts will be required for connection. Requires enabling VPN." = "Подключаться только к onion хостам. Требуется включенный VPN.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Onion hosts will be used when available. Requires enabling VPN." = "Onion хосты используются, если возможно. Требуется включенный VPN.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Onion hosts will not be used." = "Onion хосты не используются.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**";
|
||||
|
||||
@@ -992,6 +1064,12 @@
|
||||
/* call status */
|
||||
"rejected call" = "отклонённый звонок";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Relay server is only used if necessary. Another party can observe your IP address." = "Relay сервер используется только при необходимости. Другая сторона может видеть ваш IP адрес.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Relay server protects your IP address, but it can observe the duration of the call." = "Relay сервер защищает ваш IP адрес, но может отслеживать продолжительность звонка.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Remove" = "Удалить";
|
||||
|
||||
@@ -1010,9 +1088,15 @@
|
||||
/* rcv group event chat item */
|
||||
"removed you" = "удалил(а) вас из группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
/* chat item action */
|
||||
"Reply" = "Ответить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Required" = "Обязательно";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Reset colors" = "Сбросить цвета";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Reset to defaults" = "Сбросить настройки";
|
||||
|
||||
@@ -1028,7 +1112,7 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Run chat" = "Запустить chat";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
/* chat item action */
|
||||
"Save" = "Сохранить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -1049,12 +1133,18 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Scan QR code" = "Сканировать QR код";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Search" = "Поиск";
|
||||
|
||||
/* network option */
|
||||
"sec" = "сек";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"secret" = "секрет";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send direct message" = "Отправить сообщение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send link previews" = "Отправлять картинки ссылок";
|
||||
|
||||
@@ -1073,13 +1163,16 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Servers" = "Серверы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Set contact name…" = "Имя контакта…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Settings" = "Настройки";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
/* chat item action */
|
||||
"Share" = "Поделиться";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
@@ -1148,6 +1241,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Tap to join" = "Нажмите, чтобы вступить";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Tap to join incognito" = "Нажмите, чтобы вступить инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"TCP connection timeout" = "Таймаут TCP соединения";
|
||||
|
||||
@@ -1211,6 +1307,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"To ask any questions and to receive updates:" = "Чтобы задать вопросы и получать уведомления о новых версиях,";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"To make a new connection" = "Чтобы соединиться";
|
||||
|
||||
@@ -1259,6 +1358,12 @@
|
||||
/* authentication reason */
|
||||
"Unlock" = "Разблокировать";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unmute" = "Уведомлять";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update .onion hosts setting?" = "Обновить настройки .onion хостов?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update network settings?" = "Обновить настройки сети?";
|
||||
|
||||
@@ -1266,7 +1371,13 @@
|
||||
"updated group profile" = "обновил(а) профиль группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к переподключению клиента ко всем серверам.";
|
||||
"Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Updating this setting will re-connect the client to all servers." = "Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Use .onion hosts" = "Использовать .onion хосты";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Use chat" = "Использовать чат";
|
||||
@@ -1274,6 +1385,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Using .onion hosts requires compatible VPN provider." = "Для использования .onion хостов требуется совместимый VPN провайдер.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Using SimpleX Chat servers." = "Используются серверы, предоставленные SimpleX Chat.";
|
||||
|
||||
@@ -1289,6 +1403,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"via relay" = "через relay сервер";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Video call" = "Видеозвонок";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"video call (not e2e encrypted)" = "видеозвонок (не e2e зашифрованный)";
|
||||
|
||||
@@ -1310,6 +1427,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome %@!" = "Здравствуйте %@!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"When available" = "Когда возможно";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"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" = "Вы";
|
||||
|
||||
@@ -1329,7 +1452,7 @@
|
||||
"You are invited to group" = "Вы приглашены в группу";
|
||||
|
||||
/* 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**.";
|
||||
"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**.";
|
||||
|
||||
/* notification body */
|
||||
"You can now send messages to %@" = "Вы теперь можете отправлять сообщения %@";
|
||||
@@ -1379,6 +1502,9 @@
|
||||
/* chat list item description */
|
||||
"you shared one-time link" = "вы создали ссылку";
|
||||
|
||||
/* chat list item description */
|
||||
"you shared one-time link incognito" = "вы создали ссылку инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected when your connection request is accepted, please wait or check later!" = "Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!";
|
||||
|
||||
@@ -1394,6 +1520,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"you: " = "вы: ";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"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. */
|
||||
"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" = "Ваши звонки";
|
||||
|
||||
@@ -1406,6 +1538,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat profile" = "Ваш профиль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat profile will be sent to group members" = "Ваш профиль чата будет отправлен членам группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your chat profile will be sent to your contact" = "Ваш профиль будет отправлен вашему контакту";
|
||||
|
||||
@@ -1413,7 +1548,7 @@
|
||||
"Your chats" = "Ваши чаты";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your contact can scan it from the app" = "Ваш контакт может сосканировать QR в приложении";
|
||||
"Your contact can scan it from the app." = "Ваш контакт может сосканировать QR код в приложении";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Ваш контакт должен быть в сети чтобы установить соединение.\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой).";
|
||||
@@ -1436,6 +1571,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Your profile, contacts and delivered messages are stored on your device." = "Ваш профиль, контакты и доставленные сообщения хранятся на вашем устройстве.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your random profile" = "Ваш случайный профиль";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your settings" = "Настройки";
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef
|
||||
tag: f2c1455a2755e1275983dc154321fc0a5c0d7b17
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -108,12 +108,6 @@
|
||||
"invitedMember": {"ref": "memberIdRole"},
|
||||
"connRequest": {"ref": "connReqUri"},
|
||||
"groupProfile": {"ref": "profile"}
|
||||
},
|
||||
"optionalProperties": {
|
||||
"fromMemberProfile": {"ref": "profile"},
|
||||
"metadata": {
|
||||
"comment": "fromMemberProfile is user's custom profile to be used in the group - invitee should use this profile for the host's group member"
|
||||
}
|
||||
}
|
||||
},
|
||||
"memberIdRole": {
|
||||
@@ -323,12 +317,6 @@
|
||||
"params": {
|
||||
"properties": {
|
||||
"memberId": {"ref": "base64url"}
|
||||
},
|
||||
"optionalProperties": {
|
||||
"memberProfile": {"ref": "profile"},
|
||||
"metadata": {
|
||||
"comment": "memberProfile is user's custom profile to be used in the group - host should use this profile for the invitee's group member"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 3.2.0
|
||||
version: 3.2.1
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef" = "0iqk58dhckpij9l1z8bm83hghw5cwj9hmpkbk7j8vws123g1bd73";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."f2c1455a2755e1275983dc154321fc0a5c0d7b17" = "10l74d751jmgsr0ifyprglsvqdpcir86qs1vkwc4dn4n4q503p5q";
|
||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
||||
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";
|
||||
|
||||
+4
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 3.2.0
|
||||
version: 3.2.1
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -45,6 +45,9 @@ library
|
||||
Simplex.Chat.Migrations.M20220811_chat_items_indices
|
||||
Simplex.Chat.Migrations.M20220812_incognito_profiles
|
||||
Simplex.Chat.Migrations.M20220818_chat_notifications
|
||||
Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id
|
||||
Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items
|
||||
Simplex.Chat.Migrations.M20220824_profiles_local_alias
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Options
|
||||
Simplex.Chat.ProfileGenerator
|
||||
|
||||
+60
-76
@@ -578,6 +578,11 @@ processChatCommand = \case
|
||||
withCurrentCall contactId $ \userId ct call ->
|
||||
updateCallItemStatus userId ct call receivedStatus Nothing $> Just call
|
||||
APIUpdateProfile profile -> withUser (`updateProfile` profile)
|
||||
APISetContactAlias contactId localAlias -> withUser $ \User {userId} -> do
|
||||
ct' <- withStore $ \db -> do
|
||||
ct <- getContact db userId contactId
|
||||
liftIO $ updateContactAlias db userId ct localAlias
|
||||
pure $ CRContactAliasUpdated ct'
|
||||
APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text
|
||||
APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken
|
||||
APIRegisterToken token mode -> CRNtfTokenStatus <$> withUser (\_ -> withAgent $ \a -> registerNtfToken a token mode)
|
||||
@@ -619,13 +624,11 @@ processChatCommand = \case
|
||||
ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db userId contactId
|
||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||
connectionStats <- withAgent (`getConnectionServers` contactConnId ct)
|
||||
pure $ CRContactInfo ct connectionStats incognitoProfile
|
||||
APIGroupMemberInfo gId gMemberId -> withUser $ \user@User {userId} -> do
|
||||
-- [incognito] print group member main profile
|
||||
(g, m@GroupMember {memberContactProfileId}) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
|
||||
mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing
|
||||
pure $ CRContactInfo ct connectionStats (fmap fromLocalProfile incognitoProfile)
|
||||
APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do
|
||||
(g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
|
||||
connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m)
|
||||
pure $ CRGroupMemberInfo g m connectionStats mainProfile
|
||||
pure $ CRGroupMemberInfo g m connectionStats
|
||||
ContactInfo cName -> withUser $ \User {userId} -> do
|
||||
contactId <- withStore $ \db -> getContactIdByName db userId cName
|
||||
processChatCommand $ APIContactInfo contactId
|
||||
@@ -719,32 +722,29 @@ processChatCommand = \case
|
||||
processChatCommand $ APIUpdateChatItem chatRef editedItemId mc
|
||||
NewGroup gProfile -> withUser $ \user -> do
|
||||
gVar <- asks idsDrg
|
||||
-- [incognito] create membership with incognito profile
|
||||
incognito <- readTVarIO =<< asks incognitoMode
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
groupInfo <- withStore (\db -> createNewGroup db gVar user gProfile incognitoProfile)
|
||||
pure $ CRGroupCreated groupInfo incognitoProfile
|
||||
groupInfo <- withStore (\db -> createNewGroup db gVar user gProfile)
|
||||
pure $ CRGroupCreated groupInfo
|
||||
APIAddMember groupId contactId memRole -> withUser $ \user@User {userId} -> withChatLock $ do
|
||||
-- TODO for large groups: no need to load all members to determine if contact is a member
|
||||
(group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db userId contactId
|
||||
-- [incognito] forbid to invite contact to whom user is connected as incognito if user's membership is not incognito
|
||||
let Group gInfo@GroupInfo {localDisplayName, groupProfile, membership} members = group
|
||||
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
Contact {localDisplayName = cName} = contact
|
||||
when (contactConnIncognito contact && not (memberIncognito membership)) $ throwChatError CEGroupNotIncognitoCantInvite
|
||||
-- [incognito] forbid to invite contact to whom user is connected incognito
|
||||
when (contactConnIncognito contact) $ throwChatError CEContactIncognitoCantInvite
|
||||
-- [incognito] forbid to invite contacts if user joined the group using an incognito profile
|
||||
when (memberIncognito membership) $ throwChatError CEGroupIncognitoCantInvite
|
||||
when (userRole < GRAdmin || userRole < memRole) $ throwChatError CEGroupUserRole
|
||||
when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo)
|
||||
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
|
||||
let sendInvitation member@GroupMember {groupMemberId, memberId} cReq = do
|
||||
-- [incognito] if membership is incognito, send its incognito profile in GroupInvitation
|
||||
let incognitoProfile = if memberIncognito membership then Just (fromLocalProfile $ memberProfile membership) else Nothing
|
||||
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) incognitoProfile (MemberIdRole memberId memRole) cReq groupProfile
|
||||
let groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile
|
||||
msg <- sendDirectContactMessage contact $ XGrpInv groupInv
|
||||
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just $ memberIncognito membership}) memRole
|
||||
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
|
||||
ci <- saveSndChatItem user (CDDirectSnd contact) msg content Nothing Nothing
|
||||
toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat contact) ci
|
||||
setActive $ ActiveG localDisplayName
|
||||
pure $ CRSentGroupInvitation gInfo contact member incognitoProfile
|
||||
pure $ CRSentGroupInvitation gInfo contact member
|
||||
case contactMember contact members of
|
||||
Nothing -> do
|
||||
gVar <- asks idsDrg
|
||||
@@ -760,24 +760,13 @@ processChatCommand = \case
|
||||
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do
|
||||
ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId
|
||||
withChatLock . procCmd $ do
|
||||
-- [incognito] if incognito mode is enabled [AND membership is not incognito] update membership to use incognito profile
|
||||
incognito <- readTVarIO =<< asks incognitoMode
|
||||
g'@GroupInfo {membership = membership'} <-
|
||||
if incognito && not (memberIncognito membership)
|
||||
then do
|
||||
incognitoProfile <- liftIO generateRandomProfile
|
||||
membership' <- withStore $ \db -> createMemberIncognitoProfile db userId membership (Just incognitoProfile)
|
||||
pure g {membership = membership'}
|
||||
else pure g
|
||||
-- [incognito] if membership is incognito, send its incognito profile in XGrpAcpt
|
||||
let incognitoProfile = if memberIncognito membership' then Just (fromLocalProfile $ memberProfile membership') else Nothing
|
||||
agentConnId <- withAgent $ \a -> joinConnection a True connRequest . directMessage $ XGrpAcpt (memberId (membership' :: GroupMember)) incognitoProfile
|
||||
agentConnId <- withAgent $ \a -> joinConnection a True connRequest . directMessage $ XGrpAcpt (memberId (membership :: GroupMember))
|
||||
withStore' $ \db -> do
|
||||
createMemberConnection db userId fromMember agentConnId
|
||||
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership' GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
updateCIGroupInvitationStatus user
|
||||
pure $ CRUserAcceptedGroupSent g' {membership = membership' {memberStatus = GSMemAccepted}}
|
||||
pure $ CRUserAcceptedGroupSent g {membership = membership {memberStatus = GSMemAccepted}}
|
||||
where
|
||||
updateCIGroupInvitationStatus user@User {userId} = do
|
||||
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db user groupId
|
||||
@@ -984,11 +973,11 @@ processChatCommand = \case
|
||||
unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f
|
||||
(,) <$> getFileSize fsFilePath <*> asks (fileChunkSize . config)
|
||||
updateProfile :: User -> Profile -> m ChatResponse
|
||||
updateProfile user@User {profile = p@LocalProfile {profileId}} p'@Profile {displayName}
|
||||
updateProfile user@User {profile = p@LocalProfile {profileId, localAlias}} p'@Profile {displayName}
|
||||
| p' == fromLocalProfile p = pure CRUserProfileNoChange
|
||||
| otherwise = do
|
||||
withStore $ \db -> updateUserProfile db user p'
|
||||
let user' = (user :: User) {localDisplayName = displayName, profile = toLocalProfile profileId p'}
|
||||
let user' = (user :: User) {localDisplayName = displayName, profile = toLocalProfile profileId p' localAlias}
|
||||
asks currentUser >>= atomically . (`writeTVar` Just user')
|
||||
-- [incognito] filter out contacts with whom user has incognito connections
|
||||
contacts <-
|
||||
@@ -1365,7 +1354,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
CONF confId _ connInfo -> do
|
||||
-- [incognito] send saved profile
|
||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||
let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile
|
||||
let profileToSend = fromLocalProfile $ fromMaybe profile incognitoProfile
|
||||
saveConnInfo conn connInfo
|
||||
allowAgentConnection conn confId $ XInfo profileToSend
|
||||
INFO connInfo ->
|
||||
@@ -1430,7 +1419,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
Nothing -> do
|
||||
-- [incognito] print incognito profile used for this contact
|
||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||
toView $ CRContactConnected ct incognitoProfile
|
||||
toView $ CRContactConnected ct (fmap fromLocalProfile incognitoProfile)
|
||||
setActive $ ActiveC c
|
||||
showToast (c <> "> ") "connected"
|
||||
forM_ viaUserContactLink $ \userContactLinkId -> do
|
||||
@@ -1440,10 +1429,11 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) Nothing Nothing
|
||||
toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci
|
||||
_ -> pure ()
|
||||
Just (gInfo, m@GroupMember {activeConn}) -> do
|
||||
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) -> do
|
||||
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
|
||||
notifyMemberConnected gInfo m
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
|
||||
let connectedIncognito = contactConnIncognito ct || memberIncognito membership
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito
|
||||
SENT msgId -> do
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
withStore' (\db -> getDirectChatItemByAgentMsgId db userId contactId connId msgId) >>= \case
|
||||
@@ -1466,18 +1456,15 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
_ -> pure ()
|
||||
|
||||
processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m ()
|
||||
processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership, chatSettings} m@GroupMember {memberContactProfileId} = case agentMsg of
|
||||
processGroupMessage agentMsg conn gInfo@GroupInfo {groupId, localDisplayName = gName, membership, chatSettings} m = case agentMsg of
|
||||
CONF confId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
|
||||
case memberCategory m of
|
||||
GCInviteeMember ->
|
||||
case chatMsgEvent of
|
||||
XGrpAcpt memId incognitoProfile
|
||||
XGrpAcpt memId
|
||||
| sameMemberId memId m -> do
|
||||
-- [incognito] update member profile to incognito profile
|
||||
withStore $ \db -> do
|
||||
liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
|
||||
void $ createMemberIncognitoProfile db userId m incognitoProfile
|
||||
withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
|
||||
allowAgentConnection conn confId XOk
|
||||
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
|
||||
_ -> messageError "CONF from invited member must have x.grp.acpt"
|
||||
@@ -1486,7 +1473,6 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
XGrpMemInfo memId _memProfile
|
||||
| sameMemberId memId m -> do
|
||||
-- TODO update member profile
|
||||
-- [incognito] send membership incognito profile
|
||||
allowAgentConnection conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
|
||||
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
|
||||
_ -> messageError "CONF from member must have x.grp.mem.info"
|
||||
@@ -1511,17 +1497,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
unless (enableNtfs chatSettings) . withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) False
|
||||
case memberCategory m of
|
||||
GCHostMember -> do
|
||||
-- [incognito] chat item & event with indication that host connected incognito
|
||||
mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing
|
||||
memberConnectedChatItem gInfo m mainProfile
|
||||
toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} (memberIncognito membership)
|
||||
memberConnectedChatItem gInfo m
|
||||
toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected}
|
||||
setActive $ ActiveG gName
|
||||
showToast ("#" <> gName) "you are connected to group"
|
||||
GCInviteeMember -> do
|
||||
-- [incognito] chat item & event with indication that invitee connected incognito
|
||||
mainProfile <- if memberIncognito m then Just <$> withStore (\db -> getProfileById db userId memberContactProfileId) else pure Nothing
|
||||
memberConnectedChatItem gInfo m mainProfile
|
||||
toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected} mainProfile
|
||||
memberConnectedChatItem gInfo m
|
||||
toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected}
|
||||
setActive $ ActiveG gName
|
||||
showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected"
|
||||
intros <- withStore' $ \db -> createIntroductions db members m
|
||||
@@ -1539,7 +1521,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
Just ct@Contact {activeConn = Connection {connStatus}} ->
|
||||
when (connStatus == ConnReady) $ do
|
||||
notifyMemberConnected gInfo m
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
|
||||
let connectedIncognito = contactConnIncognito ct || memberIncognito membership
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody
|
||||
withAckMessage agentConnId msgMeta $
|
||||
@@ -1708,12 +1691,10 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
cancelRcvFileTransfer user ft
|
||||
throwChatError $ CEFileRcvChunk err
|
||||
|
||||
memberConnectedChatItem :: GroupInfo -> GroupMember -> Maybe Profile -> m ()
|
||||
memberConnectedChatItem gInfo m mainProfile_ = do
|
||||
memberConnectedChatItem :: GroupInfo -> GroupMember -> m ()
|
||||
memberConnectedChatItem gInfo m = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
let content = CIRcvGroupEvent $ case mainProfile_ of
|
||||
Just mainProfile -> RGEMemberConnected $ Just mainProfile
|
||||
_ -> RGEMemberConnected Nothing
|
||||
let content = CIRcvGroupEvent RGEMemberConnected
|
||||
cd = CDGroupRcv gInfo m
|
||||
-- first ts should be broker ts but we don't have it for CON
|
||||
ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content createdAt createdAt
|
||||
@@ -1722,20 +1703,24 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
|
||||
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
|
||||
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do
|
||||
memberConnectedChatItem gInfo m Nothing
|
||||
memberConnectedChatItem gInfo m
|
||||
toView $ CRConnectedToGroupMember gInfo m
|
||||
let g = groupName' gInfo
|
||||
setActive $ ActiveG g
|
||||
showToast ("#" <> g) $ "member " <> c <> " is connected"
|
||||
|
||||
probeMatchingContacts :: Contact -> m ()
|
||||
probeMatchingContacts ct = do
|
||||
probeMatchingContacts :: Contact -> Bool -> m ()
|
||||
probeMatchingContacts ct connectedIncognito = do
|
||||
gVar <- asks idsDrg
|
||||
(probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId ct
|
||||
void . sendDirectContactMessage ct $ XInfoProbe probe
|
||||
cs <- withStore' $ \db -> getMatchingContacts db userId ct
|
||||
let probeHash = ProbeHash $ C.sha256Hash (unProbe probe)
|
||||
forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ())
|
||||
if connectedIncognito
|
||||
then
|
||||
withStore' $ \db -> deleteSentProbe db userId probeId
|
||||
else do
|
||||
cs <- withStore' $ \db -> getMatchingContacts db userId ct
|
||||
let probeHash = ProbeHash $ C.sha256Hash (unProbe probe)
|
||||
forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ())
|
||||
where
|
||||
sendProbeHash :: Contact -> ProbeHash -> Int64 -> m ()
|
||||
sendProbeHash c probeHash probeId = do
|
||||
@@ -1910,20 +1895,17 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci
|
||||
|
||||
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||
processGroupInvitation ct@Contact {contactId, localDisplayName = c} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), fromMemberProfile, invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do
|
||||
processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {customUserProfileId}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
|
||||
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
|
||||
-- [incognito] if (received group invitation has host's incognito profile OR direct connection with host is incognito), create membership with new incognito profile; incognito mode is checked when joining group
|
||||
hostContact <- withStore $ \db -> getContact db userId contactId
|
||||
let joinGroupIncognito = isJust fromMemberProfile || contactConnIncognito hostContact
|
||||
incognitoProfile <- if joinGroupIncognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv incognitoProfile
|
||||
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, invitedIncognito = Just joinGroupIncognito}) memRole
|
||||
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
|
||||
gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = GroupMember {groupMemberId}} <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId
|
||||
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
|
||||
ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content Nothing
|
||||
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
|
||||
toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci
|
||||
toView $ CRReceivedGroupInvitation gInfo ct memRole fromMemberProfile
|
||||
toView $ CRReceivedGroupInvitation gInfo ct memRole
|
||||
showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group"
|
||||
|
||||
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
|
||||
@@ -2106,8 +2088,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
else do
|
||||
(groupConnId, groupConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation
|
||||
(directConnId, directConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation
|
||||
-- [incognito] direct connection with member has to be established using same incognito profile
|
||||
customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing
|
||||
-- [incognito] direct connection with member has to be established using the same incognito profile [that was known to host and used for group membership]
|
||||
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
|
||||
newMember <- withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnId directConnId customUserProfileId
|
||||
let msg = XGrpMemInv memId IntroInvitation {groupConnReq, directConnReq}
|
||||
void $ sendDirectMessage conn msg (GroupId groupId)
|
||||
@@ -2141,7 +2123,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
|
||||
groupConnId <- withAgent $ \a -> joinConnection a True groupConnReq $ directMessage msg
|
||||
directConnId <- withAgent $ \a -> joinConnection a True directConnReq $ directMessage msg
|
||||
customUserProfileId <- if memberIncognito membership then Just <$> withStore (\db -> getGroupMemberProfileId db userId membership) else pure Nothing
|
||||
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
|
||||
withStore' $ \db -> createIntroToMemberContact db userId m toMember groupConnId directConnId customUserProfileId
|
||||
|
||||
xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> MsgMeta -> m ()
|
||||
@@ -2575,6 +2557,7 @@ chatCommandP =
|
||||
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
|
||||
"/_call get" $> APIGetCallInvitations,
|
||||
"/_profile " *> (APIUpdateProfile <$> jsonP),
|
||||
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||
"/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString),
|
||||
"/_ntf get" $> APIGetNtfToken,
|
||||
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
|
||||
@@ -2685,6 +2668,7 @@ chatCommandP =
|
||||
fullNameP name = do
|
||||
n <- (A.space *> A.takeByteString) <|> pure ""
|
||||
pure $ if B.null n then name else safeDecodeUtf8 n
|
||||
textP = safeDecodeUtf8 <$> A.takeByteString
|
||||
filePath = T.unpack . safeDecodeUtf8 <$> A.takeByteString
|
||||
searchP = T.unpack . safeDecodeUtf8 <$> (" search=" *> A.takeByteString)
|
||||
memberRole =
|
||||
|
||||
@@ -133,6 +133,7 @@ data ChatCommand
|
||||
| APIGetCallInvitations
|
||||
| APICallStatus ContactId WebRTCCallStatus
|
||||
| APIUpdateProfile Profile
|
||||
| APISetContactAlias ContactId LocalAlias
|
||||
| APIParseMarkdown Text
|
||||
| APIGetNtfToken
|
||||
| APIRegisterToken DeviceToken NotificationsMode
|
||||
@@ -214,7 +215,7 @@ data ChatResponse
|
||||
| CRUserSMPServers {smpServers :: [SMPServer]}
|
||||
| CRNetworkConfig {networkConfig :: NetworkConfig}
|
||||
| CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile}
|
||||
| CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats, mainProfile :: Maybe Profile}
|
||||
| CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
|
||||
| CRNewChatItem {chatItem :: AChatItem}
|
||||
| CRChatItemStatusUpdated {chatItem :: AChatItem}
|
||||
| CRChatItemUpdated {chatItem :: AChatItem}
|
||||
@@ -226,7 +227,7 @@ data ChatResponse
|
||||
| CRCmdOk
|
||||
| CRChatHelp {helpSection :: HelpSection}
|
||||
| CRWelcome {user :: User}
|
||||
| CRGroupCreated {groupInfo :: GroupInfo, customUserProfile :: Maybe Profile}
|
||||
| CRGroupCreated {groupInfo :: GroupInfo}
|
||||
| CRGroupMembers {group :: Group}
|
||||
| CRContactsList {contacts :: [Contact]}
|
||||
| CRUserContactLink {connReqContact :: ConnReqContact, autoAccept :: Bool, autoReply :: Maybe MsgContent}
|
||||
@@ -235,7 +236,7 @@ data ChatResponse
|
||||
| CRUserAcceptedGroupSent {groupInfo :: GroupInfo}
|
||||
| CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupsList {groups :: [GroupInfo]}
|
||||
| CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember, sentCustomProfile :: Maybe Profile}
|
||||
| CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
| CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
||||
| CRUserProfile {profile :: Profile}
|
||||
| CRUserProfileNoChange
|
||||
@@ -267,6 +268,7 @@ data ChatResponse
|
||||
| CRSndFileRcvCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRSndGroupFileCancelled {chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]}
|
||||
| CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile}
|
||||
| CRContactAliasUpdated {toContact :: Contact}
|
||||
| CRContactConnecting {contact :: Contact}
|
||||
| CRContactConnected {contact :: Contact, userCustomProfile :: Maybe Profile}
|
||||
| CRContactAnotherClient {contact :: Contact}
|
||||
@@ -277,9 +279,9 @@ data ChatResponse
|
||||
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||
| CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||
| CRGroupInvitation {groupInfo :: GroupInfo}
|
||||
| CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole, receivedCustomProfile :: Maybe Profile}
|
||||
| CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember, usedCustomProfile :: Bool}
|
||||
| CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember, mainProfile :: Maybe Profile}
|
||||
| CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole}
|
||||
| CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRJoinedGroupMemberConnecting {groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember}
|
||||
| CRConnectedToGroupMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRDeletedMember {groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember}
|
||||
@@ -386,7 +388,8 @@ data ChatErrorType
|
||||
| CEContactNotReady {contact :: Contact}
|
||||
| CEContactGroups {contact :: Contact, groupNames :: [GroupName]}
|
||||
| CEGroupUserRole
|
||||
| CEGroupNotIncognitoCantInvite
|
||||
| CEContactIncognitoCantInvite
|
||||
| CEGroupIncognitoCantInvite
|
||||
| CEGroupContactRole {contactName :: ContactName}
|
||||
| CEGroupDuplicateMember {contactName :: ContactName}
|
||||
| CEGroupDuplicateMemberId
|
||||
|
||||
@@ -502,9 +502,7 @@ ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayN
|
||||
rcvGroupEventToText :: RcvGroupEvent -> Text
|
||||
rcvGroupEventToText = \case
|
||||
RGEMemberAdded _ p -> "added " <> profileToText p
|
||||
RGEMemberConnected contactMainProfile -> case contactMainProfile of
|
||||
Just p -> profileToText p <> " connected incognito"
|
||||
Nothing -> "connected"
|
||||
RGEMemberConnected -> "connected"
|
||||
RGEMemberLeft -> "left"
|
||||
RGEMemberDeleted _ p -> "removed " <> profileToText p
|
||||
RGEUserDeleted -> "removed you"
|
||||
@@ -521,6 +519,8 @@ profileToText :: Profile -> Text
|
||||
profileToText Profile {displayName, fullName} = displayName <> optionalFullName displayName fullName
|
||||
|
||||
-- This type is used both in API and in DB, so we use different JSON encodings for the database and for the API
|
||||
-- ! Nested sum types also have to use different encodings for database and API
|
||||
-- ! to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent
|
||||
data CIContent (d :: MsgDirection) where
|
||||
CISndMsgContent :: MsgContent -> CIContent 'MDSnd
|
||||
CIRcvMsgContent :: MsgContent -> CIContent 'MDRcv
|
||||
@@ -533,12 +533,15 @@ data CIContent (d :: MsgDirection) where
|
||||
CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd
|
||||
CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv
|
||||
CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd
|
||||
-- ^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API
|
||||
-- ! ^ Nested sum types also have to use different encodings for database and API
|
||||
-- ! ^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent
|
||||
|
||||
deriving instance Show (CIContent d)
|
||||
|
||||
data RcvGroupEvent
|
||||
= RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting
|
||||
| RGEMemberConnected {contactMainProfile :: Maybe Profile} -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember
|
||||
| RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember
|
||||
| RGEMemberLeft -- CRLeftMember
|
||||
| RGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRDeletedMember
|
||||
| RGEUserDeleted -- CRDeletedMemberUser
|
||||
@@ -553,6 +556,15 @@ instance ToJSON RcvGroupEvent where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RGE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RGE"
|
||||
|
||||
newtype DBRcvGroupEvent = RGE RcvGroupEvent
|
||||
|
||||
instance FromJSON DBRcvGroupEvent where
|
||||
parseJSON v = RGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RGE") v
|
||||
|
||||
instance ToJSON DBRcvGroupEvent where
|
||||
toJSON (RGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RGE") v
|
||||
toEncoding (RGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RGE") v
|
||||
|
||||
data SndGroupEvent
|
||||
= SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMember
|
||||
| SGEUserLeft -- CRLeftMemberUser
|
||||
@@ -566,13 +578,21 @@ instance ToJSON SndGroupEvent where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SGE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SGE"
|
||||
|
||||
newtype DBSndGroupEvent = SGE SndGroupEvent
|
||||
|
||||
instance FromJSON DBSndGroupEvent where
|
||||
parseJSON v = SGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "SGE") v
|
||||
|
||||
instance ToJSON DBSndGroupEvent where
|
||||
toJSON (SGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SGE") v
|
||||
toEncoding (SGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SGE") v
|
||||
|
||||
data CIGroupInvitation = CIGroupInvitation
|
||||
{ groupId :: GroupId,
|
||||
groupMemberId :: GroupMemberId,
|
||||
localDisplayName :: GroupName,
|
||||
groupProfile :: GroupProfile,
|
||||
status :: CIGroupInvitationStatus,
|
||||
invitedIncognito :: Maybe Bool
|
||||
status :: CIGroupInvitationStatus
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
@@ -703,8 +723,8 @@ data DBJSONCIContent
|
||||
| DBJCIRcvIntegrityError {msgError :: MsgErrorType}
|
||||
| DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| DBJCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent}
|
||||
| DBJCISndGroupEvent {sndGroupEvent :: SndGroupEvent}
|
||||
| DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent}
|
||||
| DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent}
|
||||
deriving (Generic)
|
||||
|
||||
instance FromJSON DBJSONCIContent where
|
||||
@@ -725,8 +745,8 @@ dbJsonCIContent = \case
|
||||
CIRcvIntegrityError err -> DBJCIRcvIntegrityError err
|
||||
CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole}
|
||||
CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole}
|
||||
CIRcvGroupEvent rcvGroupEvent -> DBJCIRcvGroupEvent {rcvGroupEvent}
|
||||
CISndGroupEvent sndGroupEvent -> DBJCISndGroupEvent {sndGroupEvent}
|
||||
CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge
|
||||
CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge
|
||||
|
||||
aciContentDBJSON :: DBJSONCIContent -> ACIContent
|
||||
aciContentDBJSON = \case
|
||||
@@ -739,8 +759,8 @@ aciContentDBJSON = \case
|
||||
DBJCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err
|
||||
DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
|
||||
DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
||||
DBJCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent
|
||||
DBJCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent
|
||||
DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge
|
||||
DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge
|
||||
|
||||
data CICallStatus
|
||||
= CISCallPending
|
||||
|
||||
@@ -10,7 +10,7 @@ m20220812_incognito_profiles =
|
||||
[sql|
|
||||
ALTER TABLE connections ADD COLUMN custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- only set for direct connections
|
||||
|
||||
ALTER TABLE group_members ADD COLUMN member_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- member profile id if incognito profile was saved for member (used for hosts and invitees in incognito groups)
|
||||
ALTER TABLE group_members ADD COLUMN member_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- member profile id if incognito profile was saved for member (used when invitation is received via incognito direct connection with host)
|
||||
|
||||
ALTER TABLE contact_profiles ADD COLUMN incognito INTEGER; -- 1 for incognito
|
||||
|]
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20220822_groups_host_conn_custom_user_profile_id :: Query
|
||||
m20220822_groups_host_conn_custom_user_profile_id =
|
||||
[sql|
|
||||
ALTER TABLE groups ADD COLUMN host_conn_custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; -- id of custom user profile used in direct connection with host
|
||||
|]
|
||||
@@ -0,0 +1,13 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20220823_delete_broken_group_event_chat_items :: Query
|
||||
m20220823_delete_broken_group_event_chat_items =
|
||||
[sql|
|
||||
DELETE FROM chat_items WHERE item_content LIKE '%{"rcvGroupEvent":{"rcvGroupEvent":{%';
|
||||
DELETE FROM chat_items WHERE item_content LIKE '%{"sndGroupEvent":{"sndGroupEvent":{%';
|
||||
|]
|
||||
@@ -0,0 +1,17 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20220824_profiles_local_alias where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20220824_profiles_local_alias :: Query
|
||||
m20220824_profiles_local_alias =
|
||||
[sql|
|
||||
PRAGMA ignore_check_constraints=ON;
|
||||
|
||||
ALTER TABLE contact_profiles ADD COLUMN local_alias TEXT DEFAULT '' CHECK (local_alias NOT NULL);
|
||||
UPDATE contact_profiles SET local_alias = '';
|
||||
|
||||
PRAGMA ignore_check_constraints=OFF;
|
||||
|]
|
||||
@@ -14,7 +14,8 @@ CREATE TABLE contact_profiles(
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
image TEXT,
|
||||
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE,
|
||||
incognito INTEGER
|
||||
incognito INTEGER,
|
||||
local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL)
|
||||
);
|
||||
CREATE INDEX contact_profiles_index ON contact_profiles(
|
||||
display_name,
|
||||
@@ -121,7 +122,8 @@ CREATE TABLE groups(
|
||||
created_at TEXT CHECK(created_at NOT NULL),
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE SET NULL,
|
||||
enable_ntfs INTEGER, -- received
|
||||
enable_ntfs INTEGER,
|
||||
host_conn_custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- received
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE CASCADE
|
||||
|
||||
@@ -124,7 +124,7 @@ data ChatMsgEvent
|
||||
| XInfo Profile
|
||||
| XContact Profile (Maybe XContactId)
|
||||
| XGrpInv GroupInvitation
|
||||
| XGrpAcpt MemberId (Maybe Profile)
|
||||
| XGrpAcpt MemberId
|
||||
| XGrpMemNew MemberInfo
|
||||
| XGrpMemIntro MemberInfo
|
||||
| XGrpMemInv MemberId IntroInvitation
|
||||
@@ -413,7 +413,7 @@ toCMEventTag = \case
|
||||
XInfo _ -> XInfo_
|
||||
XContact _ _ -> XContact_
|
||||
XGrpInv _ -> XGrpInv_
|
||||
XGrpAcpt _ _ -> XGrpAcpt_
|
||||
XGrpAcpt _ -> XGrpAcpt_
|
||||
XGrpMemNew _ -> XGrpMemNew_
|
||||
XGrpMemIntro _ -> XGrpMemIntro_
|
||||
XGrpMemInv _ _ -> XGrpMemInv_
|
||||
@@ -479,7 +479,7 @@ appToChatMessage AppMessage {msgId, event, params} = do
|
||||
XInfo_ -> XInfo <$> p "profile"
|
||||
XContact_ -> XContact <$> p "profile" <*> opt "contactReqId"
|
||||
XGrpInv_ -> XGrpInv <$> p "groupInvitation"
|
||||
XGrpAcpt_ -> XGrpAcpt <$> p "memberId" <*> opt "memberProfile"
|
||||
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
|
||||
XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo"
|
||||
XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo"
|
||||
XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro"
|
||||
@@ -521,7 +521,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = AppMessage {msgId, event, p
|
||||
XInfo profile -> o ["profile" .= profile]
|
||||
XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile]
|
||||
XGrpInv groupInv -> o ["groupInvitation" .= groupInv]
|
||||
XGrpAcpt memId profile -> o $ ("memberProfile" .=? profile) ["memberId" .= memId]
|
||||
XGrpAcpt memId -> o ["memberId" .= memId]
|
||||
XGrpMemNew memInfo -> o ["memberInfo" .= memInfo]
|
||||
XGrpMemIntro memInfo -> o ["memberInfo" .= memInfo]
|
||||
XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro]
|
||||
|
||||
+109
-115
@@ -38,6 +38,7 @@ module Simplex.Chat.Store
|
||||
getContactIdByName,
|
||||
updateUserProfile,
|
||||
updateContactProfile,
|
||||
updateContactAlias,
|
||||
getUserContacts,
|
||||
createUserContactLink,
|
||||
getUserContactLinkConnections,
|
||||
@@ -81,14 +82,12 @@ module Simplex.Chat.Store
|
||||
getMemberInvitation,
|
||||
createMemberConnection,
|
||||
updateGroupMemberStatus,
|
||||
createMemberIncognitoProfile,
|
||||
createNewGroupMember,
|
||||
deleteGroupMember,
|
||||
deleteGroupMemberConnection,
|
||||
createIntroductions,
|
||||
updateIntroStatus,
|
||||
saveIntroInvitation,
|
||||
getGroupMemberProfileId,
|
||||
createIntroReMember,
|
||||
createIntroToMemberContact,
|
||||
saveMemberInvitation,
|
||||
@@ -98,6 +97,7 @@ module Simplex.Chat.Store
|
||||
randomBytes,
|
||||
createSentProbe,
|
||||
createSentProbeHash,
|
||||
deleteSentProbe,
|
||||
matchReceivedProbe,
|
||||
matchReceivedProbeHash,
|
||||
matchSentProbe,
|
||||
@@ -230,6 +230,9 @@ import Simplex.Chat.Migrations.M20220715_groups_chat_item_id
|
||||
import Simplex.Chat.Migrations.M20220811_chat_items_indices
|
||||
import Simplex.Chat.Migrations.M20220812_incognito_profiles
|
||||
import Simplex.Chat.Migrations.M20220818_chat_notifications
|
||||
import Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id
|
||||
import Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items
|
||||
import Simplex.Chat.Migrations.M20220824_profiles_local_alias
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..))
|
||||
@@ -261,7 +264,10 @@ schemaMigrations =
|
||||
("20220715_groups_chat_item_id", m20220715_groups_chat_item_id),
|
||||
("20220811_chat_items_indices", m20220811_chat_items_indices),
|
||||
("20220812_incognito_profiles", m20220812_incognito_profiles),
|
||||
("20220818_chat_notifications", m20220818_chat_notifications)
|
||||
("20220818_chat_notifications", m20220818_chat_notifications),
|
||||
("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id),
|
||||
("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items),
|
||||
("20220824_profiles_local_alias", m20220824_profiles_local_alias)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -327,7 +333,7 @@ getUsers db =
|
||||
|
||||
toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData) -> User
|
||||
toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image}
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias = ""}
|
||||
in User {userId, userContactId, localDisplayName = displayName, profile, activeUser}
|
||||
|
||||
setActiveUser :: DB.Connection -> UserId -> IO ()
|
||||
@@ -366,7 +372,7 @@ getConnReqContactXContactId db userId cReqHash = do
|
||||
[sql|
|
||||
SELECT
|
||||
-- Contact
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
@@ -412,20 +418,20 @@ createIncognitoProfile_ db userId createdAt incognitoProfile =
|
||||
(displayName, fullName, image, userId, Just True, createdAt, createdAt)
|
||||
insertedRowId db
|
||||
|
||||
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO Profile
|
||||
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
|
||||
getProfileById db userId profileId =
|
||||
ExceptT . firstRow toProfile (SEProfileNotFound profileId) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT display_name, full_name, image
|
||||
SELECT display_name, full_name, image, local_alias
|
||||
FROM contact_profiles
|
||||
WHERE user_id = ? AND contact_profile_id = ?
|
||||
|]
|
||||
(userId, profileId)
|
||||
where
|
||||
toProfile :: (ContactName, Text, Maybe ImageData) -> Profile
|
||||
toProfile (displayName, fullName, image) = Profile {displayName, fullName, image}
|
||||
toProfile :: (ContactName, Text, Maybe ImageData, LocalAlias) -> LocalProfile
|
||||
toProfile (displayName, fullName, image, localAlias) = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
|
||||
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection
|
||||
createConnection_ db userId connType entityId acId viaContact viaUserContactLink customUserProfileId connLevel currentTs = do
|
||||
@@ -449,7 +455,7 @@ createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> Excep
|
||||
createDirectContact db userId activeConn@Connection {connId} profile = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
(localDisplayName, contactId, profileId) <- createContact_ db userId connId profile Nothing createdAt
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt, updatedAt = createdAt}
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt, updatedAt = createdAt}
|
||||
|
||||
createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId)
|
||||
createContact_ db userId connId Profile {displayName, fullName, image} viaGroup currentTs =
|
||||
@@ -532,15 +538,28 @@ updateUserProfile db User {userId, userContactId, localDisplayName, profile = Lo
|
||||
updateContact_ db userId userContactId localDisplayName newName currentTs
|
||||
|
||||
updateContactProfile :: DB.Connection -> UserId -> Contact -> Profile -> ExceptT StoreError IO Contact
|
||||
updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName}} p'@Profile {displayName = newName}
|
||||
updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}} p'@Profile {displayName = newName}
|
||||
| displayName == newName =
|
||||
liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p'}
|
||||
liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p' localAlias}
|
||||
| otherwise =
|
||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||
currentTs <- getCurrentTime
|
||||
updateContactProfile_' db userId profileId p' currentTs
|
||||
updateContact_ db userId contactId localDisplayName ldn currentTs
|
||||
pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p'}
|
||||
pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p' localAlias}
|
||||
|
||||
updateContactAlias :: DB.Connection -> UserId -> Contact -> LocalAlias -> IO Contact
|
||||
updateContactAlias db userId c@Contact {profile = lp@LocalProfile {profileId}} localAlias = do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET local_alias = ?, updated_at = ?
|
||||
WHERE user_id = ? AND contact_profile_id = ?
|
||||
|]
|
||||
(localAlias, updatedAt, userId, profileId)
|
||||
pure $ (c :: Contact) {profile = lp {localAlias = localAlias}}
|
||||
|
||||
updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO ()
|
||||
updateContactProfile_ db userId profileId profile = do
|
||||
@@ -570,18 +589,18 @@ updateContact_ db userId contactId displayName newName updatedAt = do
|
||||
(newName, updatedAt, userId, contactId)
|
||||
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (displayName, userId)
|
||||
|
||||
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime)
|
||||
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, LocalAlias, Maybe Bool, UTCTime, UTCTime)
|
||||
|
||||
toContact :: ContactRow :. ConnectionRow -> Contact
|
||||
toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image}
|
||||
toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
activeConn = toConnection connRow
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt}
|
||||
|
||||
toContactOrError :: ContactRow :. MaybeConnectionRow -> Either StoreError Contact
|
||||
toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image}
|
||||
toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in case toMaybeConnection connRow of
|
||||
Just activeConn ->
|
||||
@@ -765,7 +784,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN
|
||||
[sql|
|
||||
SELECT
|
||||
-- Contact
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
@@ -888,7 +907,7 @@ createAcceptedContact db userId agentConnId localDisplayName profileId profile u
|
||||
(userId, localDisplayName, profileId, True, createdAt, createdAt, xContactId)
|
||||
contactId <- insertedRowId db
|
||||
activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile, activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt = createdAt, updatedAt = createdAt}
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, chatSettings = defaultChatSettings, createdAt = createdAt, updatedAt = createdAt}
|
||||
|
||||
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
|
||||
getLiveSndFileTransfers db User {userId} = do
|
||||
@@ -1026,6 +1045,13 @@ createSentProbeHash db userId probeId _to@Contact {contactId} = do
|
||||
"INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?)"
|
||||
(probeId, contactId, userId, currentTs, currentTs)
|
||||
|
||||
deleteSentProbe :: DB.Connection -> UserId -> Int64 -> IO ()
|
||||
deleteSentProbe db userId probeId =
|
||||
DB.execute
|
||||
db
|
||||
"DELETE FROM sent_probes WHERE user_id = ? AND sent_probe_id = ?"
|
||||
(userId, probeId)
|
||||
|
||||
matchReceivedProbe :: DB.Connection -> UserId -> Contact -> Probe -> IO (Maybe Contact)
|
||||
matchReceivedProbe db userId _from@Contact {contactId} (Probe probe) = do
|
||||
let probeHash = C.sha256Hash probe
|
||||
@@ -1159,15 +1185,15 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, c.via_group, c.enable_ntfs, c.created_at, c.updated_at
|
||||
SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, c.via_group, c.enable_ntfs, c.created_at, c.updated_at
|
||||
FROM contacts c
|
||||
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
|
||||
WHERE c.user_id = ? AND c.contact_id = ?
|
||||
|]
|
||||
(userId, contactId)
|
||||
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime)] -> Either StoreError Contact
|
||||
toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt)] =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image}
|
||||
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Maybe Bool, UTCTime, UTCTime)] -> Either StoreError Contact
|
||||
toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, enableNtfs_, createdAt, updatedAt)] =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt}
|
||||
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
|
||||
@@ -1179,15 +1205,15 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
-- GroupInfo {membership}
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
-- GroupInfo {membership = GroupMember {memberProfile}}
|
||||
pu.display_name, pu.full_name, pu.image,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias,
|
||||
-- from GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
JOIN groups g ON g.group_id = m.group_id
|
||||
@@ -1267,15 +1293,15 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
-- GroupInfo {membership}
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
-- GroupInfo {membership = GroupMember {memberProfile}}
|
||||
pu.display_name, pu.full_name, pu.image,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias,
|
||||
-- from GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
FROM group_members m
|
||||
@@ -1305,8 +1331,8 @@ updateConnectionStatus db Connection {connId} connStatus = do
|
||||
DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId)
|
||||
|
||||
-- | creates completely new group with a single member - the current user
|
||||
createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo
|
||||
createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do
|
||||
createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||
createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do
|
||||
let GroupProfile {displayName, fullName, image} = groupProfile
|
||||
currentTs <- getCurrentTime
|
||||
withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
|
||||
@@ -1322,14 +1348,13 @@ createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = Except
|
||||
(ldn, userId, profileId, True, currentTs, currentTs)
|
||||
insertedRowId db
|
||||
memberId <- liftIO $ encodedRandomBytes gVar 12
|
||||
-- TODO ldn from incognito profile
|
||||
membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser incognitoProfile currentTs
|
||||
membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs
|
||||
let chatSettings = ChatSettings {enableNtfs = True}
|
||||
pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs}
|
||||
pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs}
|
||||
|
||||
-- | creates a new group record for the group the current user was invited to, or returns an existing one
|
||||
createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe Profile -> ExceptT StoreError IO GroupInfo
|
||||
createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInvitation {fromMember, fromMemberProfile, invitedMember, connRequest, groupProfile} incognitoProfile = do
|
||||
createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO GroupInfo
|
||||
createGroupInvitation db user@User {userId} contact@Contact {contactId, activeConn = Connection {customUserProfileId}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do
|
||||
liftIO getInvitationGroupId_ >>= \case
|
||||
Nothing -> createGroupInvitation_
|
||||
-- TODO treat the case that the invitation details could've changed
|
||||
@@ -1353,20 +1378,20 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId} GroupInv
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(profileId, localDisplayName, connRequest, userId, True, currentTs, currentTs)
|
||||
"INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs)
|
||||
insertedRowId db
|
||||
_ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown fromMemberProfile currentTs
|
||||
membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfile currentTs
|
||||
_ <- createContactMemberInv_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs
|
||||
membership <- createContactMemberInv_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs
|
||||
let chatSettings = ChatSettings {enableNtfs = True}
|
||||
pure GroupInfo {groupId, localDisplayName, groupProfile, membership, chatSettings, createdAt = currentTs, updatedAt = currentTs}
|
||||
pure GroupInfo {groupId, localDisplayName, groupProfile, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs}
|
||||
|
||||
createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe Profile -> UTCTime -> ExceptT StoreError IO GroupMember
|
||||
createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfile createdAt = do
|
||||
customUserProfileId <- liftIO $ createIncognitoProfile_ db userId createdAt incognitoProfile
|
||||
(localDisplayName, memberProfile) <- case (incognitoProfile, customUserProfileId) of
|
||||
(Just profile@Profile {displayName}, Just profileId) ->
|
||||
(,toLocalProfile profileId profile) <$> insertMemberIncognitoProfile_ displayName profileId
|
||||
createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> ExceptT StoreError IO GroupMember
|
||||
createContactMemberInv_ db User {userId, userContactId} groupId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt = do
|
||||
incognitoProfile <- forM incognitoProfileId $ \profileId -> getProfileById db userId profileId
|
||||
(localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of
|
||||
(Just profile@LocalProfile {displayName}, Just profileId) ->
|
||||
(,profile) <$> insertMemberIncognitoProfile_ displayName profileId
|
||||
_ -> (,profile' userOrContact) <$> liftIO insertMember_
|
||||
groupMemberId <- liftIO $ insertedRowId db
|
||||
pure
|
||||
@@ -1470,14 +1495,14 @@ getUserGroupDetails db User {userId, userContactId} =
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
m.group_member_id, g.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, mp.contact_profile_id, mp.display_name, mp.full_name, mp.image
|
||||
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status,
|
||||
mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.local_alias
|
||||
FROM groups g
|
||||
JOIN group_profiles gp USING (group_profile_id)
|
||||
JOIN group_members m USING (group_id)
|
||||
JOIN contact_profiles mp ON mp.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
WHERE g.user_id = ? AND m.contact_id = ?
|
||||
JOIN group_members mu USING (group_id)
|
||||
JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id)
|
||||
WHERE g.user_id = ? AND mu.contact_id = ?
|
||||
|]
|
||||
(userId, userContactId)
|
||||
|
||||
@@ -1486,13 +1511,13 @@ getGroupInfoByName db user gName = do
|
||||
gId <- getGroupIdByName db user gName
|
||||
getGroupInfo db user gId
|
||||
|
||||
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe Bool, UTCTime, UTCTime) :. GroupMemberRow
|
||||
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, UTCTime, UTCTime) :. GroupMemberRow
|
||||
|
||||
toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo
|
||||
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, enableNtfs_, createdAt, updatedAt) :. userMemberRow) =
|
||||
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, hostConnCustomUserProfileId, enableNtfs_, createdAt, updatedAt) :. userMemberRow) =
|
||||
let membership = toGroupMember userContactId userMemberRow
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, chatSettings, createdAt, updatedAt}
|
||||
in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
|
||||
|
||||
getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
||||
getGroupMember db user@User {userId} groupId groupMemberId =
|
||||
@@ -1502,7 +1527,7 @@ getGroupMember db user@User {userId} groupId groupMemberId =
|
||||
[sql|
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
FROM group_members m
|
||||
@@ -1524,7 +1549,7 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
[sql|
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
FROM group_members m
|
||||
@@ -1562,20 +1587,20 @@ getGroupInvitation db user groupId = do
|
||||
findFromContact (IBContact contactId) = find ((== Just contactId) . memberContactId)
|
||||
findFromContact _ = const Nothing
|
||||
|
||||
type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData))
|
||||
type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, LocalAlias))
|
||||
|
||||
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus) :. (Maybe Int64, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData))
|
||||
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus) :. (Maybe Int64, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe LocalAlias))
|
||||
|
||||
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
|
||||
toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image)) =
|
||||
let memberProfile = LocalProfile {profileId, displayName, fullName, image}
|
||||
toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias)) =
|
||||
let memberProfile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
invitedBy = toInvitedBy userContactId invitedById
|
||||
activeConn = Nothing
|
||||
in GroupMember {..}
|
||||
|
||||
toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
|
||||
toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus) :. (invitedById, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image)) =
|
||||
Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image))
|
||||
toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just memberRole, Just memberCategory, Just memberStatus) :. (invitedById, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, Just localAlias)) =
|
||||
Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias))
|
||||
toMaybeGroupMember _ _ = Nothing
|
||||
|
||||
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember
|
||||
@@ -1640,25 +1665,6 @@ updateGroupMemberStatus db userId GroupMember {groupMemberId} memStatus = do
|
||||
|]
|
||||
(memStatus, currentTs, userId, groupMemberId)
|
||||
|
||||
createMemberIncognitoProfile :: DB.Connection -> UserId -> GroupMember -> Maybe Profile -> ExceptT StoreError IO GroupMember
|
||||
createMemberIncognitoProfile db userId m@GroupMember {groupMemberId} incognitoProfile = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
customUserProfileId <- liftIO $ createIncognitoProfile_ db userId currentTs incognitoProfile
|
||||
case (incognitoProfile, customUserProfileId) of
|
||||
(Just profile@Profile {displayName}, Just profileId) ->
|
||||
ExceptT $
|
||||
withLocalDisplayName db userId displayName $ \incognitoLdn -> do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET local_display_name = ?, member_profile_id = ?, updated_at = ?
|
||||
WHERE user_id = ? AND group_member_id = ?
|
||||
|]
|
||||
(incognitoLdn, profileId, currentTs, userId, groupMemberId)
|
||||
pure . Right $ m {localDisplayName = incognitoLdn, memberProfile = toLocalProfile profileId profile}
|
||||
_ -> pure m
|
||||
|
||||
-- | add new member with profile
|
||||
createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember
|
||||
createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image}) memCategory memStatus =
|
||||
@@ -1708,7 +1714,7 @@ createNewMember_
|
||||
|]
|
||||
(groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt)
|
||||
groupMemberId <- insertedRowId db
|
||||
pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile, memberContactId, memberContactProfileId, activeConn}
|
||||
pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile "", memberContactId, memberContactProfileId, activeConn}
|
||||
|
||||
deleteGroupMember :: DB.Connection -> User -> GroupMember -> IO ()
|
||||
deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId} = do
|
||||
@@ -1814,18 +1820,6 @@ getIntroduction_ db reMember toMember = ExceptT $ do
|
||||
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
|
||||
toIntro _ = Left SEIntroNotFound
|
||||
|
||||
getGroupMemberProfileId :: DB.Connection -> UserId -> GroupMember -> ExceptT StoreError IO Int64
|
||||
getGroupMemberProfileId db userId GroupMember {groupMemberId, groupId} =
|
||||
ExceptT . firstRow fromOnly (SEGroupMemberNotFound {groupId, groupMemberId}) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT contact_profile_id
|
||||
FROM group_members
|
||||
WHERE user_id = ? AND group_member_id = ?
|
||||
|]
|
||||
(userId, groupMemberId)
|
||||
|
||||
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> ConnId -> ConnId -> Maybe ProfileId -> ExceptT StoreError IO GroupMember
|
||||
createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId customUserProfileId = do
|
||||
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
|
||||
@@ -1892,15 +1886,15 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
-- GroupInfo {membership}
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
-- GroupInfo {membership = GroupMember {memberProfile}}
|
||||
pu.display_name, pu.full_name, pu.image,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias,
|
||||
-- via GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image,
|
||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
FROM group_members m
|
||||
@@ -1932,7 +1926,7 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} =
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, ct.via_group, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, ct.via_group, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
FROM contacts ct
|
||||
@@ -1948,9 +1942,9 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} =
|
||||
|]
|
||||
(userId, groupMemberId)
|
||||
where
|
||||
toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe Int64, Maybe Bool, UTCTime, UTCTime) :. ConnectionRow -> Contact
|
||||
toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, viaGroup, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image}
|
||||
toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Maybe Bool, UTCTime, UTCTime) :. ConnectionRow -> Contact
|
||||
toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
activeConn = toConnection connRow
|
||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt}
|
||||
@@ -2654,7 +2648,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
|
||||
-- GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
|
||||
p.display_name, p.full_name, p.image
|
||||
p.display_name, p.full_name, p.image, p.local_alias
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
LEFT JOIN contacts c ON m.contact_id = c.contact_id
|
||||
@@ -2691,7 +2685,7 @@ getDirectChatPreviews_ db User {userId} = do
|
||||
[sql|
|
||||
SELECT
|
||||
-- Contact
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at,
|
||||
@@ -2756,11 +2750,11 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
-- GroupMember - membership
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
pu.display_name, pu.full_name, pu.image,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias,
|
||||
-- ChatStats
|
||||
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0),
|
||||
-- ChatItem
|
||||
@@ -2770,13 +2764,13 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
-- Maybe GroupMember - sender
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
|
||||
p.display_name, p.full_name, p.image,
|
||||
p.display_name, p.full_name, p.image, p.local_alias,
|
||||
-- quoted ChatItem
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
|
||||
-- quoted GroupMember
|
||||
rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category,
|
||||
rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
|
||||
rp.display_name, rp.full_name, rp.image
|
||||
rp.display_name, rp.full_name, rp.image, rp.local_alias
|
||||
FROM groups g
|
||||
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
|
||||
JOIN group_members mu ON mu.group_id = g.group_id
|
||||
@@ -3031,7 +3025,7 @@ getContact db userId contactId =
|
||||
[sql|
|
||||
SELECT
|
||||
-- Contact
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.enable_ntfs, ct.created_at, ct.updated_at,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.custom_user_profile_id, c.conn_status, c.conn_type,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
@@ -3156,11 +3150,11 @@ getGroupInfo db User {userId, userContactId} groupId =
|
||||
[sql|
|
||||
SELECT
|
||||
-- GroupInfo
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
-- GroupMember - membership
|
||||
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
|
||||
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
|
||||
pu.display_name, pu.full_name, pu.image
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias
|
||||
FROM groups g
|
||||
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
|
||||
JOIN group_members mu ON mu.group_id = g.group_id
|
||||
@@ -3529,13 +3523,13 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
-- GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
|
||||
p.display_name, p.full_name, p.image,
|
||||
p.display_name, p.full_name, p.image, p.local_alias,
|
||||
-- quoted ChatItem
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
|
||||
-- quoted GroupMember
|
||||
rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category,
|
||||
rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
|
||||
rp.display_name, rp.full_name, rp.image
|
||||
rp.display_name, rp.full_name, rp.image, rp.local_alias
|
||||
FROM chat_items i
|
||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
LEFT JOIN group_members m ON m.group_member_id = i.group_member_id
|
||||
|
||||
@@ -191,6 +191,7 @@ data GroupInfo = GroupInfo
|
||||
localDisplayName :: GroupName,
|
||||
groupProfile :: GroupProfile,
|
||||
membership :: GroupMember,
|
||||
hostConnCustomUserProfileId :: Maybe ProfileId,
|
||||
chatSettings :: ChatSettings,
|
||||
createdAt :: UTCTime,
|
||||
updatedAt :: UTCTime
|
||||
@@ -217,7 +218,10 @@ data Profile = Profile
|
||||
{ displayName :: ContactName,
|
||||
fullName :: Text,
|
||||
image :: Maybe ImageData
|
||||
-- incognito field should not be read as is into this data type to prevent sending it as part of profile to contacts
|
||||
-- fields that should not be read into this data type to prevent sending them as part of profile to contacts:
|
||||
-- - contact_profile_id
|
||||
-- - incognito
|
||||
-- - local_alias
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
@@ -225,11 +229,14 @@ instance ToJSON Profile where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
type LocalAlias = Text
|
||||
|
||||
data LocalProfile = LocalProfile
|
||||
{ profileId :: ProfileId,
|
||||
displayName :: ContactName,
|
||||
fullName :: Text,
|
||||
image :: Maybe ImageData
|
||||
image :: Maybe ImageData,
|
||||
localAlias :: LocalAlias
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
@@ -240,9 +247,9 @@ instance ToJSON LocalProfile where
|
||||
localProfileId :: LocalProfile -> ProfileId
|
||||
localProfileId = profileId
|
||||
|
||||
toLocalProfile :: ProfileId -> Profile -> LocalProfile
|
||||
toLocalProfile profileId Profile {displayName, fullName, image} =
|
||||
LocalProfile {profileId, displayName, fullName, image}
|
||||
toLocalProfile :: ProfileId -> Profile -> LocalAlias -> LocalProfile
|
||||
toLocalProfile profileId Profile {displayName, fullName, image} localAlias =
|
||||
LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
|
||||
fromLocalProfile :: LocalProfile -> Profile
|
||||
fromLocalProfile LocalProfile {displayName, fullName, image} =
|
||||
@@ -275,7 +282,6 @@ instance FromField ImageData where fromField = fmap ImageData . fromField
|
||||
|
||||
data GroupInvitation = GroupInvitation
|
||||
{ fromMember :: MemberIdRole,
|
||||
fromMemberProfile :: Maybe Profile,
|
||||
invitedMember :: MemberIdRole,
|
||||
connRequest :: ConnReqInvitation,
|
||||
groupProfile :: GroupProfile
|
||||
@@ -758,9 +764,6 @@ data Connection = Connection
|
||||
aConnId :: Connection -> ConnId
|
||||
aConnId Connection {agentConnId = AgentConnId cId} = cId
|
||||
|
||||
connCustomUserProfileId :: Connection -> Maybe Int64
|
||||
connCustomUserProfileId Connection {customUserProfileId} = customUserProfileId
|
||||
|
||||
instance ToJSON Connection where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
+55
-75
@@ -65,7 +65,7 @@ responseToView testView = \case
|
||||
CRUserSMPServers smpServers -> viewSMPServers smpServers testView
|
||||
CRNetworkConfig cfg -> viewNetworkConfig cfg
|
||||
CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile
|
||||
CRGroupMemberInfo g m cStats mainProfile -> viewGroupMemberInfo g m cStats mainProfile
|
||||
CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats
|
||||
CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item False
|
||||
CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True) chatItems
|
||||
CRChatItemStatusUpdated _ -> []
|
||||
@@ -89,10 +89,10 @@ responseToView testView = \case
|
||||
CRUserContactLink cReqUri autoAccept autoReply -> connReqContact_ "Your chat address:" cReqUri <> autoAcceptStatus_ autoAccept autoReply
|
||||
CRUserContactLinkUpdated _ autoAccept autoReply -> autoAcceptStatus_ autoAccept autoReply
|
||||
CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"]
|
||||
CRGroupCreated g customUserProfile -> viewGroupCreated g customUserProfile testView
|
||||
CRGroupCreated g -> viewGroupCreated g
|
||||
CRGroupMembers g -> viewGroupMembers g
|
||||
CRGroupsList gs -> viewGroupsList gs
|
||||
CRSentGroupInvitation g c _ sentCustomProfile -> viewSentGroupInvitation g c sentCustomProfile
|
||||
CRSentGroupInvitation g c _ -> viewSentGroupInvitation g c
|
||||
CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus
|
||||
CRUserProfile p -> viewUserProfile p
|
||||
CRUserProfileNoChange -> ["user profile did not change"]
|
||||
@@ -117,6 +117,7 @@ responseToView testView = \case
|
||||
CRSndGroupFileCancelled _ ftm fts -> viewSndGroupFileCancelled ftm fts
|
||||
CRRcvFileCancelled ft -> receivingFile_ "cancelled" ft
|
||||
CRUserProfileUpdated p p' -> viewUserProfileUpdated p p'
|
||||
CRContactAliasUpdated c -> viewContactAliasUpdated c
|
||||
CRContactUpdated c c' -> viewContactUpdated c c'
|
||||
CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt
|
||||
CRReceivedContactRequest UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile
|
||||
@@ -138,11 +139,10 @@ responseToView testView = \case
|
||||
[sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors"
|
||||
where
|
||||
(errors, subscribed) = partition (isJust . contactError) summary
|
||||
CRGroupInvitation GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} ->
|
||||
[groupInvitation' ldn fullName $ memberIncognito membership]
|
||||
CRReceivedGroupInvitation g c role receivedCustomProfile -> viewReceivedGroupInvitation g c role receivedCustomProfile
|
||||
CRUserJoinedGroup g _ usedCustomProfile -> viewUserJoinedGroup g usedCustomProfile testView
|
||||
CRJoinedGroupMember g m mainProfile -> viewJoinedGroupMember g m mainProfile
|
||||
CRGroupInvitation g -> [groupInvitation' g]
|
||||
CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role
|
||||
CRUserJoinedGroup g _ -> viewUserJoinedGroup g
|
||||
CRJoinedGroupMember g m -> viewJoinedGroupMember g m
|
||||
CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h]
|
||||
CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h]
|
||||
CRJoinedGroupMemberConnecting g host m -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"]
|
||||
@@ -369,11 +369,9 @@ viewConnReqInvitation cReq =
|
||||
"and ask them to connect: " <> highlight' "/c <invitation_link_above>"
|
||||
]
|
||||
|
||||
viewSentGroupInvitation :: GroupInfo -> Contact -> Maybe Profile -> [StyledString]
|
||||
viewSentGroupInvitation g c sentCustomProfile =
|
||||
if isJust sentCustomProfile
|
||||
then ["invitation to join the group " <> ttyGroup' g <> " incognito sent to " <> ttyContact' c]
|
||||
else ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c]
|
||||
viewSentGroupInvitation :: GroupInfo -> Contact -> [StyledString]
|
||||
viewSentGroupInvitation g c =
|
||||
["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c]
|
||||
|
||||
viewChatCleared :: AChatInfo -> [StyledString]
|
||||
viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of
|
||||
@@ -427,22 +425,11 @@ viewReceivedContactRequest c Profile {fullName} =
|
||||
"to reject: " <> highlight ("/rc " <> c) <> " (the sender will NOT be notified)"
|
||||
]
|
||||
|
||||
viewGroupCreated :: GroupInfo -> Maybe Profile -> Bool -> [StyledString]
|
||||
viewGroupCreated g@GroupInfo {localDisplayName} incognitoProfile testView =
|
||||
case incognitoProfile of
|
||||
Just profile ->
|
||||
if testView
|
||||
then incognitoProfile' profile : message
|
||||
else message
|
||||
where
|
||||
message =
|
||||
[ "group " <> ttyFullGroup g <> " is created incognito, your profile for this group: " <> incognitoProfile' profile,
|
||||
"use " <> highlight ("/a " <> localDisplayName <> " <name>") <> " to add members"
|
||||
]
|
||||
Nothing ->
|
||||
[ "group " <> ttyFullGroup g <> " is created",
|
||||
"use " <> highlight ("/a " <> localDisplayName <> " <name>") <> " to add members"
|
||||
]
|
||||
viewGroupCreated :: GroupInfo -> [StyledString]
|
||||
viewGroupCreated g@GroupInfo {localDisplayName} =
|
||||
[ "group " <> ttyFullGroup g <> " is created",
|
||||
"use " <> highlight ("/a " <> localDisplayName <> " <name>") <> " to add members"
|
||||
]
|
||||
|
||||
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
|
||||
viewCannotResendInvitation GroupInfo {localDisplayName = gn} c =
|
||||
@@ -450,33 +437,22 @@ viewCannotResendInvitation GroupInfo {localDisplayName = gn} c =
|
||||
"to re-send invitation: " <> highlight ("/rm " <> gn <> " " <> c) <> ", " <> highlight ("/a " <> gn <> " " <> c)
|
||||
]
|
||||
|
||||
viewUserJoinedGroup :: GroupInfo -> Bool -> Bool -> [StyledString]
|
||||
viewUserJoinedGroup g@GroupInfo {membership = GroupMember {memberProfile}} incognito testView =
|
||||
if incognito
|
||||
then
|
||||
if testView
|
||||
then incognitoProfile' (fromLocalProfile memberProfile) : incognitoMessage
|
||||
else incognitoMessage
|
||||
viewUserJoinedGroup :: GroupInfo -> [StyledString]
|
||||
viewUserJoinedGroup g@GroupInfo {membership = membership@GroupMember {memberProfile}} =
|
||||
if memberIncognito membership
|
||||
then [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)]
|
||||
else [ttyGroup' g <> ": you joined the group"]
|
||||
where
|
||||
incognitoMessage = [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)]
|
||||
|
||||
viewJoinedGroupMember :: GroupInfo -> GroupMember -> Maybe Profile -> [StyledString]
|
||||
viewJoinedGroupMember g m@GroupMember {localDisplayName} = \case
|
||||
Just Profile {displayName = mainProfileName} -> [ttyGroup' g <> ": " <> ttyContact mainProfileName <> " joined the group incognito as " <> styleIncognito localDisplayName]
|
||||
Nothing -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "]
|
||||
viewJoinedGroupMember :: GroupInfo -> GroupMember -> [StyledString]
|
||||
viewJoinedGroupMember g m =
|
||||
[ttyGroup' g <> ": " <> ttyMember m <> " joined the group "]
|
||||
|
||||
viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> Maybe Profile -> [StyledString]
|
||||
viewReceivedGroupInvitation g c role hostIncognitoProfile =
|
||||
case hostIncognitoProfile of
|
||||
Just profile ->
|
||||
[ ttyFullGroup g <> ": " <> ttyContact' c <> " (known to the group as " <> incognitoProfile' profile <> ") invites you to join the group incognito as " <> plain (strEncode role),
|
||||
"use " <> highlight ("/j " <> groupName' g) <> " to join this group incognito"
|
||||
]
|
||||
Nothing ->
|
||||
[ ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role),
|
||||
"use " <> highlight ("/j " <> groupName' g) <> " to accept"
|
||||
]
|
||||
viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString]
|
||||
viewReceivedGroupInvitation g@GroupInfo {membership = membership@GroupMember {memberProfile}} c role =
|
||||
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) :
|
||||
if memberIncognito membership
|
||||
then ["use " <> highlight ("/j " <> groupName' g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)]
|
||||
else ["use " <> highlight ("/j " <> groupName' g) <> " to accept"]
|
||||
|
||||
groupPreserved :: GroupInfo -> [StyledString]
|
||||
groupPreserved g = ["use " <> highlight ("/d #" <> groupName' g) <> " to delete the group"]
|
||||
@@ -528,9 +504,9 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g <nam
|
||||
viewGroupsList gs = map groupSS $ sortOn ldn_ gs
|
||||
where
|
||||
ldn_ = T.toLower . (localDisplayName :: GroupInfo -> GroupName)
|
||||
groupSS GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} =
|
||||
groupSS g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership} =
|
||||
case memberStatus membership of
|
||||
GSMemInvited -> groupInvitation' ldn fullName $ memberIncognito membership
|
||||
GSMemInvited -> groupInvitation' g
|
||||
s -> incognito <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s
|
||||
where
|
||||
incognito = if memberIncognito membership then incognitoPrefix else ""
|
||||
@@ -541,20 +517,20 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ gs
|
||||
_ -> ""
|
||||
delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")"
|
||||
|
||||
groupInvitation' :: GroupName -> Text -> Bool -> StyledString
|
||||
groupInvitation' displayName fullName membershipIncognito =
|
||||
highlight ("#" <> displayName)
|
||||
<> optFullName displayName fullName
|
||||
<> invitationText
|
||||
<> highlight ("/j " <> displayName)
|
||||
<> " to join, "
|
||||
<> highlight ("/d #" <> displayName)
|
||||
groupInvitation' :: GroupInfo -> StyledString
|
||||
groupInvitation' GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership = membership@GroupMember {memberProfile}} =
|
||||
highlight ("#" <> ldn)
|
||||
<> optFullName ldn fullName
|
||||
<> " - you are invited ("
|
||||
<> highlight ("/j " <> ldn)
|
||||
<> joinText
|
||||
<> highlight ("/d #" <> ldn)
|
||||
<> " to delete invitation)"
|
||||
where
|
||||
invitationText =
|
||||
if membershipIncognito
|
||||
then " - you are invited incognito ("
|
||||
else " - you are invited ("
|
||||
joinText =
|
||||
if memberIncognito membership
|
||||
then " to join as " <> incognitoProfile' (fromLocalProfile memberProfile) <> ", "
|
||||
else " to join, "
|
||||
|
||||
viewContactsMerged :: Contact -> Contact -> [StyledString]
|
||||
viewContactsMerged _into@Contact {localDisplayName = c1} _merged@Contact {localDisplayName = c2} =
|
||||
@@ -594,23 +570,21 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
|
||||
]
|
||||
|
||||
viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString]
|
||||
viewContactInfo Contact {contactId} stats incognitoProfile =
|
||||
viewContactInfo Contact {contactId, profile = LocalProfile {localAlias}} stats incognitoProfile =
|
||||
["contact ID: " <> sShow contactId] <> viewConnectionStats stats
|
||||
<> maybe
|
||||
["you've shared main profile with this contact"]
|
||||
(\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p])
|
||||
incognitoProfile
|
||||
<> if localAlias /= "" then ["alias: " <> plain localAlias] else ["alias not set"]
|
||||
|
||||
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
|
||||
viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId} stats mainProfile =
|
||||
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString]
|
||||
viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats =
|
||||
[ "group ID: " <> sShow groupId,
|
||||
"member ID: " <> sShow groupMemberId
|
||||
]
|
||||
<> maybe ["member not connected"] viewConnectionStats stats
|
||||
<> maybe
|
||||
["unknown whether group member uses his main profile or incognito one for the group"]
|
||||
(\Profile {displayName, fullName} -> ["member is using " <> styleIncognito' "incognito" <> " profile for the group, main profile known: " <> ttyFullName displayName fullName])
|
||||
mainProfile
|
||||
<> if localAlias /= "" then ["alias: " <> plain localAlias] else ["no alias for contact"]
|
||||
|
||||
viewConnectionStats :: ConnectionStats -> [StyledString]
|
||||
viewConnectionStats ConnectionStats {rcvServers, sndServers} =
|
||||
@@ -644,6 +618,11 @@ viewGroupUpdated
|
||||
where
|
||||
byMember = maybe "" ((" by " <>) . ttyMember) m
|
||||
|
||||
viewContactAliasUpdated :: Contact -> [StyledString]
|
||||
viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}}
|
||||
| localAlias == "" = ["contact " <> ttyContact n <> " alias removed"]
|
||||
| otherwise = ["contact " <> ttyContact n <> " alias updated: " <> plain localAlias]
|
||||
|
||||
viewContactUpdated :: Contact -> Contact -> [StyledString]
|
||||
viewContactUpdated
|
||||
Contact {localDisplayName = n, profile = LocalProfile {fullName}}
|
||||
@@ -904,7 +883,8 @@ viewChatError = \case
|
||||
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
|
||||
CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"]
|
||||
CEGroupUserRole -> ["you have insufficient permissions for this group command"]
|
||||
CEGroupNotIncognitoCantInvite -> ["you're using main profile for this group - prohibited to invite contact to whom you are connected incognito"]
|
||||
CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"]
|
||||
CEGroupIncognitoCantInvite -> ["you've connected to this group using an incognito profile - prohibited to invite contacts"]
|
||||
CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"]
|
||||
CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> groupName' g)]
|
||||
CEGroupMemberNotActive -> ["you cannot invite other members yet, try later"]
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef
|
||||
commit: f2c1455a2755e1275983dc154321fc0a5c0d7b17
|
||||
# - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
|
||||
- github: simplex-chat/aeson
|
||||
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7
|
||||
|
||||
+150
-220
@@ -92,9 +92,9 @@ chatTests = do
|
||||
it "connect incognito via invitation link" testConnectIncognitoInvitationLink
|
||||
it "connect incognito via contact address" testConnectIncognitoContactAddress
|
||||
it "accept contact request incognito" testAcceptContactRequestIncognito
|
||||
it "create group incognito" testCreateGroupIncognito
|
||||
it "join group incognito" testJoinGroupIncognito
|
||||
it "can't invite contact to whom user connected incognito to non incognito group" testCantInviteIncognitoConnectionNonIncognitoGroup
|
||||
it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito
|
||||
it "set contact alias" testSetAlias
|
||||
describe "SMP servers" $
|
||||
it "get and set SMP servers" testGetSetSMPServers
|
||||
describe "async connection handshake" $ do
|
||||
@@ -2146,278 +2146,199 @@ testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $
|
||||
bob #> ("@" <> aliceIncognito <> " I know!")
|
||||
alice ?<# "bob> I know!"
|
||||
|
||||
testCreateGroupIncognito :: IO ()
|
||||
testCreateGroupIncognito = testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
-- non incognito connections
|
||||
connectUsers alice cath
|
||||
connectUsers bob cath
|
||||
-- bob connected incognito to alice
|
||||
alice ##> "/c"
|
||||
inv <- getInvitation alice
|
||||
bob #$> ("/incognito on", id, "ok")
|
||||
bob ##> ("/c " <> inv)
|
||||
bob <## "confirmation sent!"
|
||||
bobIncognito <- getTermLine bob
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /info alice to print out this incognito profile again",
|
||||
alice <## (bobIncognito <> ": contact is connected")
|
||||
]
|
||||
-- alice creates group incognito
|
||||
alice #$> ("/incognito on", id, "ok")
|
||||
alice ##> "/g secret_club"
|
||||
aliceMemIncognito <- getTermLine alice
|
||||
alice <## ("group #secret_club is created incognito, your profile for this group: " <> aliceMemIncognito)
|
||||
alice <## "use /a secret_club <name> to add members"
|
||||
alice ##> ("/a secret_club " <> bobIncognito)
|
||||
concurrentlyN_
|
||||
[ alice <## ("invitation to join the group #secret_club incognito sent to " <> bobIncognito),
|
||||
do
|
||||
bob <## ("#secret_club: alice (known to the group as " <> aliceMemIncognito <> ") invites you to join the group incognito as admin")
|
||||
bob <## "use /j secret_club to join this group incognito"
|
||||
]
|
||||
-- bob uses different profile when joining group
|
||||
bob ##> "/j secret_club"
|
||||
bobMemIncognito <- getTermLine bob
|
||||
concurrently_
|
||||
(alice <## ("#secret_club: " <> bobIncognito <> " joined the group incognito as " <> bobMemIncognito))
|
||||
(bob <## ("#secret_club: you joined the group incognito as " <> bobMemIncognito))
|
||||
-- cath is invited incognito
|
||||
alice ##> "/a secret_club cath"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #secret_club incognito sent to cath",
|
||||
do
|
||||
cath <## ("#secret_club: alice (known to the group as " <> aliceMemIncognito <> ") invites you to join the group incognito as admin")
|
||||
cath <## "use /j secret_club to join this group incognito"
|
||||
]
|
||||
cath ##> "/j secret_club"
|
||||
cathMemIncognito <- getTermLine cath
|
||||
-- bob and cath don't merge contacts
|
||||
concurrentlyN_
|
||||
[ alice <## ("#secret_club: cath joined the group incognito as " <> cathMemIncognito),
|
||||
do
|
||||
cath <## ("#secret_club: you joined the group incognito as " <> cathMemIncognito)
|
||||
cath <## ("#secret_club: member " <> bobMemIncognito <> " is connected"),
|
||||
do
|
||||
bob <## ("#secret_club: " <> aliceMemIncognito <> " added " <> cathMemIncognito <> " to the group (connecting...)")
|
||||
bob <## ("#secret_club: new member " <> cathMemIncognito <> " is connected")
|
||||
]
|
||||
-- send messages - group is incognito for everybody
|
||||
alice #$> ("/incognito off", id, "ok")
|
||||
bob #$> ("/incognito off", id, "ok")
|
||||
cath #$> ("/incognito off", id, "ok")
|
||||
alice ?#> "#secret_club hello"
|
||||
concurrently_
|
||||
(bob ?<# ("#secret_club " <> aliceMemIncognito <> "> hello"))
|
||||
(cath ?<# ("#secret_club " <> aliceMemIncognito <> "> hello"))
|
||||
bob ?#> "#secret_club hi there"
|
||||
concurrently_
|
||||
(alice ?<# ("#secret_club " <> bobMemIncognito <> "> hi there"))
|
||||
(cath ?<# ("#secret_club " <> bobMemIncognito <> "> hi there"))
|
||||
cath ?#> "#secret_club hey"
|
||||
concurrently_
|
||||
(alice ?<# ("#secret_club " <> cathMemIncognito <> "> hey"))
|
||||
(bob ?<# ("#secret_club " <> cathMemIncognito <> "> hey"))
|
||||
-- bob and cath can send messages via direct incognito connections
|
||||
bob ?#> ("@" <> cathMemIncognito <> " hi, I'm bob")
|
||||
cath ?<# (bobMemIncognito <> "> hi, I'm bob")
|
||||
cath ?#> ("@" <> bobMemIncognito <> " hey, I'm cath")
|
||||
bob ?<# (cathMemIncognito <> "> hey, I'm cath")
|
||||
-- non incognito connections are separate
|
||||
bob <##> cath
|
||||
-- list groups
|
||||
alice ##> "/gs"
|
||||
alice <## "i #secret_club"
|
||||
-- list group members
|
||||
alice ##> "/ms secret_club"
|
||||
alice
|
||||
<### [ "i " <> aliceMemIncognito <> ": owner, you, created group",
|
||||
"i " <> bobMemIncognito <> ": admin, invited, connected",
|
||||
"i " <> cathMemIncognito <> ": admin, invited, connected"
|
||||
]
|
||||
-- remove member
|
||||
bob ##> ("/rm secret_club " <> cathMemIncognito)
|
||||
concurrentlyN_
|
||||
[ bob <## ("#secret_club: you removed " <> cathMemIncognito <> " from the group"),
|
||||
alice <## ("#secret_club: " <> bobMemIncognito <> " removed " <> cathMemIncognito <> " from the group"),
|
||||
do
|
||||
cath <## ("#secret_club: " <> bobMemIncognito <> " removed you from the group")
|
||||
cath <## "use /d #secret_club to delete the group"
|
||||
]
|
||||
bob ?#> "#secret_club hi"
|
||||
concurrently_
|
||||
(alice ?<# ("#secret_club " <> bobMemIncognito <> "> hi"))
|
||||
(cath </)
|
||||
alice ?#> "#secret_club hello"
|
||||
concurrently_
|
||||
(bob ?<# ("#secret_club " <> aliceMemIncognito <> "> hello"))
|
||||
(cath </)
|
||||
cath ##> "#secret_club hello"
|
||||
cath <## "you are no longer a member of the group"
|
||||
bob ?#> ("@" <> cathMemIncognito <> " I removed you from group")
|
||||
cath ?<# (bobMemIncognito <> "> I removed you from group")
|
||||
cath ?#> ("@" <> bobMemIncognito <> " ok")
|
||||
bob ?<# (cathMemIncognito <> "> ok")
|
||||
|
||||
testJoinGroupIncognito :: IO ()
|
||||
testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $
|
||||
\alice bob cath dan -> do
|
||||
-- non incognito connections
|
||||
connectUsers alice cath
|
||||
connectUsers alice bob
|
||||
connectUsers alice dan
|
||||
connectUsers bob cath
|
||||
connectUsers bob dan
|
||||
connectUsers cath dan
|
||||
-- bob connected incognito to alice
|
||||
-- cath connected incognito to alice
|
||||
alice ##> "/c"
|
||||
inv <- getInvitation alice
|
||||
bob #$> ("/incognito on", id, "ok")
|
||||
bob ##> ("/c " <> inv)
|
||||
bob <## "confirmation sent!"
|
||||
bobIncognito <- getTermLine bob
|
||||
cath #$> ("/incognito on", id, "ok")
|
||||
cath ##> ("/c " <> inv)
|
||||
cath <## "confirmation sent!"
|
||||
cathIncognito <- getTermLine cath
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /info alice to print out this incognito profile again",
|
||||
alice <## (bobIncognito <> ": contact is connected")
|
||||
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
|
||||
cath <## "use /info alice to print out this incognito profile again",
|
||||
alice <## (cathIncognito <> ": contact is connected")
|
||||
]
|
||||
-- alice creates group non incognito
|
||||
alice ##> "/g club"
|
||||
alice <## "group #club is created"
|
||||
alice <## "use /a club <name> to add members"
|
||||
alice ##> ("/a club " <> bobIncognito)
|
||||
-- alice creates group
|
||||
alice ##> "/g secret_club"
|
||||
alice <## "group #secret_club is created"
|
||||
alice <## "use /a secret_club <name> to add members"
|
||||
-- alice invites bob
|
||||
alice ##> "/a secret_club bob"
|
||||
concurrentlyN_
|
||||
[ alice <## ("invitation to join the group #club sent to " <> bobIncognito),
|
||||
[ alice <## "invitation to join the group #secret_club sent to bob",
|
||||
do
|
||||
bob <## "#club: alice invites you to join the group as admin"
|
||||
bob <## "use /j club to accept"
|
||||
bob <## "#secret_club: alice invites you to join the group as admin"
|
||||
bob <## "use /j secret_club to accept"
|
||||
]
|
||||
-- since bob is connected incognito to host, he uses different profile when joining group even though he turned incognito mode off
|
||||
bob #$> ("/incognito off", id, "ok")
|
||||
bob ##> "/j club"
|
||||
bobMemIncognito <- getTermLine bob
|
||||
bob ##> "/j secret_club"
|
||||
concurrently_
|
||||
(alice <## ("#club: " <> bobIncognito <> " joined the group incognito as " <> bobMemIncognito))
|
||||
(bob <## ("#club: you joined the group incognito as " <> bobMemIncognito))
|
||||
-- cath joins incognito
|
||||
alice ##> "/a club cath"
|
||||
(alice <## "#secret_club: bob joined the group")
|
||||
(bob <## "#secret_club: you joined the group")
|
||||
-- alice invites cath
|
||||
alice ##> ("/a secret_club " <> cathIncognito)
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #club sent to cath",
|
||||
[ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
|
||||
do
|
||||
cath <## "#club: alice invites you to join the group as admin"
|
||||
cath <## "use /j club to accept"
|
||||
cath <## "#secret_club: alice invites you to join the group as admin"
|
||||
cath <## ("use /j secret_club to join incognito as " <> cathIncognito)
|
||||
]
|
||||
cath #$> ("/incognito on", id, "ok")
|
||||
cath ##> "/j club"
|
||||
cathMemIncognito <- getTermLine cath
|
||||
-- bob and cath don't merge contacts
|
||||
-- cath uses the same incognito profile when joining group, disabling incognito mode doesn't affect it
|
||||
cath #$> ("/incognito off", id, "ok")
|
||||
cath ##> "/j secret_club"
|
||||
-- cath and bob don't merge contacts
|
||||
concurrentlyN_
|
||||
[ alice <## ("#club: cath joined the group incognito as " <> cathMemIncognito),
|
||||
[ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"),
|
||||
do
|
||||
cath <## ("#club: you joined the group incognito as " <> cathMemIncognito)
|
||||
cath <## ("#club: member " <> bobMemIncognito <> " is connected"),
|
||||
cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito)
|
||||
cath <## "#secret_club: member bob_1 (Bob) is connected",
|
||||
do
|
||||
bob <## ("#club: alice added " <> cathMemIncognito <> " to the group (connecting...)")
|
||||
bob <## ("#club: new member " <> cathMemIncognito <> " is connected")
|
||||
bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)")
|
||||
bob <## ("#secret_club: new member " <> cathIncognito <> " is connected")
|
||||
]
|
||||
-- cath invites dan incognito
|
||||
cath ##> "/a club dan"
|
||||
-- cath cannot invite to the group because her membership is incognito
|
||||
cath ##> "/a secret_club dan"
|
||||
cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts"
|
||||
-- alice invites dan
|
||||
alice ##> "/a secret_club dan"
|
||||
concurrentlyN_
|
||||
[ cath <## "invitation to join the group #club incognito sent to dan",
|
||||
[ alice <## "invitation to join the group #secret_club sent to dan",
|
||||
do
|
||||
dan <## ("#club: cath (known to the group as " <> cathMemIncognito <> ") invites you to join the group incognito as admin")
|
||||
dan <## "use /j club to join this group incognito"
|
||||
dan <## "#secret_club: alice invites you to join the group as admin"
|
||||
dan <## "use /j secret_club to accept"
|
||||
]
|
||||
dan ##> "/j club"
|
||||
danMemIncognito <- getTermLine dan
|
||||
dan ##> "/j secret_club"
|
||||
-- cath and dan don't merge contacts
|
||||
concurrentlyN_
|
||||
[ cath <## ("#club: dan joined the group incognito as " <> danMemIncognito),
|
||||
[ alice <## "#secret_club: dan joined the group",
|
||||
do
|
||||
dan <## ("#club: you joined the group incognito as " <> danMemIncognito)
|
||||
dan <## "#secret_club: you joined the group"
|
||||
dan
|
||||
<### [ "#club: member alice (Alice) is connected",
|
||||
"#club: member " <> bobMemIncognito <> " is connected"
|
||||
<### [ "#secret_club: member " <> cathIncognito <> " is connected",
|
||||
"#secret_club: member bob_1 (Bob) is connected",
|
||||
"contact bob_1 is merged into bob",
|
||||
"use @bob <message> to send messages"
|
||||
],
|
||||
do
|
||||
alice <## ("#club: " <> cathMemIncognito <> " added " <> danMemIncognito <> " to the group (connecting...)")
|
||||
alice <## ("#club: new member " <> danMemIncognito <> " is connected"),
|
||||
bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
|
||||
bob <## "#secret_club: new member dan_1 is connected"
|
||||
bob <## "contact dan_1 is merged into dan"
|
||||
bob <## "use @dan <message> to send messages",
|
||||
do
|
||||
bob <## ("#club: " <> cathMemIncognito <> " added " <> danMemIncognito <> " to the group (connecting...)")
|
||||
bob <## ("#club: new member " <> danMemIncognito <> " is connected")
|
||||
cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
|
||||
cath <## "#secret_club: new member dan_1 is connected"
|
||||
]
|
||||
-- send messages - group is incognito for cath and dan
|
||||
alice #$> ("/incognito off", id, "ok")
|
||||
bob #$> ("/incognito off", id, "ok")
|
||||
cath #$> ("/incognito off", id, "ok")
|
||||
dan #$> ("/incognito off", id, "ok")
|
||||
alice #> "#club hello"
|
||||
-- send messages - group is incognito for cath
|
||||
alice #> "#secret_club hello"
|
||||
concurrentlyN_
|
||||
[ bob ?<# "#club alice> hello",
|
||||
cath ?<# "#club alice> hello",
|
||||
dan ?<# "#club alice> hello"
|
||||
[ bob <# "#secret_club alice> hello",
|
||||
cath ?<# "#secret_club alice> hello",
|
||||
dan <# "#secret_club alice> hello"
|
||||
]
|
||||
bob ?#> "#club hi there"
|
||||
bob #> "#secret_club hi there"
|
||||
concurrentlyN_
|
||||
[ alice <# ("#club " <> bobMemIncognito <> "> hi there"),
|
||||
cath ?<# ("#club " <> bobMemIncognito <> "> hi there"),
|
||||
dan ?<# ("#club " <> bobMemIncognito <> "> hi there")
|
||||
[ alice <# "#secret_club bob> hi there",
|
||||
cath ?<# "#secret_club bob_1> hi there",
|
||||
dan <# "#secret_club bob> hi there"
|
||||
]
|
||||
cath ?#> "#club hey"
|
||||
cath ?#> "#secret_club hey"
|
||||
concurrentlyN_
|
||||
[ alice <# ("#club " <> cathMemIncognito <> "> hey"),
|
||||
bob ?<# ("#club " <> cathMemIncognito <> "> hey"),
|
||||
dan ?<# ("#club " <> cathMemIncognito <> "> hey")
|
||||
[ alice <# ("#secret_club " <> cathIncognito <> "> hey"),
|
||||
bob <# ("#secret_club " <> cathIncognito <> "> hey"),
|
||||
dan <# ("#secret_club " <> cathIncognito <> "> hey")
|
||||
]
|
||||
dan ?#> "#club how is it going?"
|
||||
dan #> "#secret_club how is it going?"
|
||||
concurrentlyN_
|
||||
[ alice <# ("#club " <> danMemIncognito <> "> how is it going?"),
|
||||
bob ?<# ("#club " <> danMemIncognito <> "> how is it going?"),
|
||||
cath ?<# ("#club " <> danMemIncognito <> "> how is it going?")
|
||||
[ alice <# "#secret_club dan> how is it going?",
|
||||
bob <# "#secret_club dan> how is it going?",
|
||||
cath ?<# "#secret_club dan_1> how is it going?"
|
||||
]
|
||||
-- bob and cath can send messages via direct incognito connections
|
||||
bob ?#> ("@" <> cathMemIncognito <> " hi, I'm bob")
|
||||
cath ?<# (bobMemIncognito <> "> hi, I'm bob")
|
||||
cath ?#> ("@" <> bobMemIncognito <> " hey, I'm cath")
|
||||
bob ?<# (cathMemIncognito <> "> hey, I'm cath")
|
||||
-- cath and bob can send messages via new direct connection, cath is incognito
|
||||
bob #> ("@" <> cathIncognito <> " hi, I'm bob")
|
||||
cath ?<# "bob_1> hi, I'm bob"
|
||||
cath ?#> "@bob_1 hey, I'm incognito"
|
||||
bob <# (cathIncognito <> "> hey, I'm incognito")
|
||||
-- cath and dan can send messages via new direct connection, cath is incognito
|
||||
dan #> ("@" <> cathIncognito <> " hi, I'm dan")
|
||||
cath ?<# "dan_1> hi, I'm dan"
|
||||
cath ?#> "@dan_1 hey, I'm incognito"
|
||||
dan <# (cathIncognito <> "> hey, I'm incognito")
|
||||
-- non incognito connections are separate
|
||||
bob <##> cath
|
||||
-- bob and dan can send messages via direct incognito connections
|
||||
bob ?#> ("@" <> danMemIncognito <> " hi, I'm bob")
|
||||
dan ?<# (bobMemIncognito <> "> hi, I'm bob")
|
||||
dan ?#> ("@" <> bobMemIncognito <> " hey, I'm dan")
|
||||
bob ?<# (danMemIncognito <> "> hey, I'm dan")
|
||||
dan <##> cath
|
||||
-- list groups
|
||||
cath ##> "/gs"
|
||||
cath <## "i #secret_club"
|
||||
-- list group members
|
||||
alice ##> "/ms club"
|
||||
alice ##> "/ms secret_club"
|
||||
alice
|
||||
<### [ "alice (Alice): owner, you, created group",
|
||||
"i " <> bobMemIncognito <> ": admin, invited, connected",
|
||||
"i " <> cathMemIncognito <> ": admin, invited, connected",
|
||||
danMemIncognito <> ": admin, connected"
|
||||
"bob (Bob): admin, invited, connected",
|
||||
cathIncognito <> ": admin, invited, connected",
|
||||
"dan (Daniel): admin, invited, connected"
|
||||
]
|
||||
bob ##> "/ms club"
|
||||
bob ##> "/ms secret_club"
|
||||
bob
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"i " <> bobMemIncognito <> ": admin, you, connected",
|
||||
cathMemIncognito <> ": admin, connected",
|
||||
danMemIncognito <> ": admin, connected"
|
||||
"bob (Bob): admin, you, connected",
|
||||
cathIncognito <> ": admin, connected",
|
||||
"dan (Daniel): admin, connected"
|
||||
]
|
||||
cath ##> "/ms club"
|
||||
cath ##> "/ms secret_club"
|
||||
cath
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
bobMemIncognito <> ": admin, connected",
|
||||
"i " <> cathMemIncognito <> ": admin, you, connected",
|
||||
"i " <> danMemIncognito <> ": admin, invited, connected"
|
||||
"bob_1 (Bob): admin, connected",
|
||||
"i " <> cathIncognito <> ": admin, you, connected",
|
||||
"dan_1 (Daniel): admin, connected"
|
||||
]
|
||||
dan ##> "/ms club"
|
||||
dan ##> "/ms secret_club"
|
||||
dan
|
||||
<### [ "alice (Alice): owner, connected",
|
||||
bobMemIncognito <> ": admin, connected",
|
||||
"i " <> cathMemIncognito <> ": admin, host, connected",
|
||||
"i " <> danMemIncognito <> ": admin, you, connected"
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"bob (Bob): admin, connected",
|
||||
cathIncognito <> ": admin, connected",
|
||||
"dan (Daniel): admin, you, connected"
|
||||
]
|
||||
-- remove member
|
||||
bob ##> ("/rm secret_club " <> cathIncognito)
|
||||
concurrentlyN_
|
||||
[ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"),
|
||||
alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
|
||||
dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
|
||||
do
|
||||
cath <## "#secret_club: bob_1 removed you from the group"
|
||||
cath <## "use /d #secret_club to delete the group"
|
||||
]
|
||||
bob #> "#secret_club hi"
|
||||
concurrentlyN_
|
||||
[ alice <# "#secret_club bob> hi",
|
||||
dan <# "#secret_club bob> hi",
|
||||
(cath </)
|
||||
]
|
||||
alice #> "#secret_club hello"
|
||||
concurrentlyN_
|
||||
[ bob <# "#secret_club alice> hello",
|
||||
dan <# "#secret_club alice> hello",
|
||||
(cath </)
|
||||
]
|
||||
cath ##> "#secret_club hello"
|
||||
cath <## "you are no longer a member of the group"
|
||||
-- cath can still message members directly
|
||||
bob #> ("@" <> cathIncognito <> " I removed you from group")
|
||||
cath ?<# "bob_1> I removed you from group"
|
||||
cath ?#> "@bob_1 ok"
|
||||
bob <# (cathIncognito <> "> ok")
|
||||
|
||||
testCantInviteIncognitoConnectionNonIncognitoGroup :: IO ()
|
||||
testCantInviteIncognitoConnectionNonIncognitoGroup = testChat2 aliceProfile bobProfile $
|
||||
testCantInviteContactIncognito :: IO ()
|
||||
testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
-- alice connected incognito to bob
|
||||
alice #$> ("/incognito on", id, "ok")
|
||||
@@ -2438,7 +2359,16 @@ testCantInviteIncognitoConnectionNonIncognitoGroup = testChat2 aliceProfile bobP
|
||||
alice <## "group #club is created"
|
||||
alice <## "use /a club <name> to add members"
|
||||
alice ##> "/a club bob"
|
||||
alice <## "you're using main profile for this group - prohibited to invite contact to whom you are connected incognito"
|
||||
alice <## "you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"
|
||||
-- bob doesn't receive invitation
|
||||
(bob </)
|
||||
|
||||
testSetAlias :: IO ()
|
||||
testSetAlias = testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
alice #$> ("/_set alias @2 my friend bob", id, "contact bob alias updated: my friend bob")
|
||||
alice #$> ("/_set alias @2", id, "contact bob alias removed")
|
||||
|
||||
testGetSetSMPServers :: IO ()
|
||||
testGetSetSMPServers =
|
||||
|
||||
@@ -31,9 +31,9 @@ activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\"
|
||||
|
||||
activeUser :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}}"
|
||||
activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"activeUser\":true}}}}"
|
||||
#else
|
||||
activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}"
|
||||
activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"activeUser\":true}}}"
|
||||
#endif
|
||||
|
||||
chatStarted :: String
|
||||
|
||||
@@ -187,18 +187,12 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
it "x.contact with content (ignored)" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
==# XContact testProfile Nothing
|
||||
it "x.grp.inv with incognito profile" $
|
||||
"{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\"},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"},\"fromMemberProfile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}}"
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, fromMemberProfile = Just testProfile, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile}
|
||||
it "x.grp.inv without incognito profile" $
|
||||
it "x.grp.inv" $
|
||||
"{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\"},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, fromMemberProfile = Nothing, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile}
|
||||
it "x.grp.acpt with incognito profile" $
|
||||
"{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\", \"memberProfile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
#==# XGrpAcpt (MemberId "\1\2\3\4") (Just testProfile)
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile}
|
||||
it "x.grp.acpt without incognito profile" $
|
||||
"{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
#==# XGrpAcpt (MemberId "\1\2\3\4") Nothing
|
||||
#==# XGrpAcpt (MemberId "\1\2\3\4")
|
||||
it "x.grp.mem.new" $
|
||||
"{\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}}"
|
||||
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
|
||||
|
||||
Reference in New Issue
Block a user