mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2f663dde3 | |||
| 038b936bfe | |||
| 20def8c7a0 | |||
| a9b4489f4f | |||
| 5ca21dea13 | |||
| 80ca80f6d8 | |||
| 687a741723 | |||
| 54ab4e979a | |||
| b6696e901b | |||
| 89de5497ef | |||
| 1bf3154488 | |||
| c78acfda33 | |||
| 1432a04927 | |||
| d432dfba21 | |||
| 5243613045 | |||
| 83599adc80 | |||
| 538992eb95 | |||
| 658daf56bb | |||
| 7d31862576 | |||
| 7a1d0eac9d | |||
| d851396113 | |||
| cbdd9b9e37 | |||
| d5fc0d7dfc | |||
| 0d0de1da86 | |||
| 4e5a5c11dc | |||
| 14038ce370 | |||
| a72f603e13 | |||
| 85609ef217 | |||
| 7631d59695 | |||
| 38f305bb34 | |||
| 290ef9de61 | |||
| f38d3b4d7f | |||
| 8f638df7a9 | |||
| 8a121b4442 | |||
| 1bb7a954f5 | |||
| ddd8bd9061 | |||
| 179b9e093f | |||
| 352a4f3d2a | |||
| e06d4e5c85 | |||
| 385ebd2298 |
@@ -108,7 +108,8 @@ jobs:
|
||||
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest'
|
||||
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
|
||||
timeout-minutes: 10
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 64
|
||||
versionName "4.2-beta.0"
|
||||
versionCode 66
|
||||
versionName "4.2-beta.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
|
||||
@@ -523,6 +523,8 @@ data class Contact(
|
||||
val activeConn: Connection,
|
||||
val viaGroup: Long? = null,
|
||||
val chatSettings: ChatSettings,
|
||||
// User applies his preferences for the contact here. Named user_preferences on the contact in DB
|
||||
val userPreferences: ChatPreferences,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -553,6 +555,7 @@ data class Contact(
|
||||
profile = LocalProfile.sampleData,
|
||||
activeConn = Connection.sampleData,
|
||||
chatSettings = ChatSettings(true),
|
||||
userPreferences = ChatPreferences(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
@@ -586,7 +589,9 @@ class Profile(
|
||||
override val displayName: String,
|
||||
override val fullName: String,
|
||||
override val image: String? = null,
|
||||
override val localAlias : String = ""
|
||||
override val localAlias : String = "",
|
||||
// Contact applies his preferences here
|
||||
val preferences: ChatPreferences? = null
|
||||
): NamedChat {
|
||||
val profileViewName: String
|
||||
get() {
|
||||
@@ -610,6 +615,8 @@ class LocalProfile(
|
||||
override val fullName: String,
|
||||
override val image: String? = null,
|
||||
override val localAlias: String,
|
||||
// Contact applies his preferences here
|
||||
val preferences: ChatPreferences? = null
|
||||
): NamedChat {
|
||||
val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" }
|
||||
|
||||
@@ -639,6 +646,7 @@ data class GroupInfo (
|
||||
val membership: GroupMember,
|
||||
val hostConnCustomUserProfileId: Long? = null,
|
||||
val chatSettings: ChatSettings,
|
||||
// val groupPreferences: GroupPreferences? = null,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -774,6 +782,12 @@ class GroupMember (
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class GroupMemberRef(
|
||||
val groupMemberId: Long,
|
||||
val profile: Profile
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class GroupMemberRole(val memberRole: String) {
|
||||
@SerialName("member") Member("member"), // order matters in comparisons
|
||||
@@ -905,6 +919,7 @@ class PendingContactConnection(
|
||||
val pccAgentConnId: String,
|
||||
val pccConnStatus: ConnStatus,
|
||||
val viaContactUri: Boolean,
|
||||
val groupLinkId: String? = null,
|
||||
val customUserProfileId: Long? = null,
|
||||
val connReqInv: String? = null,
|
||||
override val localAlias: String,
|
||||
@@ -943,8 +958,11 @@ class PendingContactConnection(
|
||||
return if (initiated == null) "" else generalGetString(
|
||||
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 (viaContactUri)
|
||||
if (groupLinkId != null)
|
||||
if (incognito) R.string.description_via_group_link_incognito else R.string.description_via_group_link
|
||||
else
|
||||
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
|
||||
)
|
||||
@@ -1213,6 +1231,8 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
|
||||
override val text: String get() = when(this) {
|
||||
is SndMsgContent -> msgContent.text
|
||||
@@ -1226,6 +1246,8 @@ sealed class CIContent: ItemContent {
|
||||
is SndGroupInvitation -> groupInvitation.text
|
||||
is RcvGroupEventContent -> rcvGroupEvent.text
|
||||
is SndGroupEventContent -> sndGroupEvent.text
|
||||
is RcvConnEventContent -> rcvConnEvent.text
|
||||
is SndConnEventContent -> sndConnEvent.text
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1584,6 +1606,46 @@ sealed class SndGroupEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class RcvConnEvent {
|
||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is SwitchQueue -> when (phase) {
|
||||
SwitchPhase.Completed -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_completed)
|
||||
else -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_changing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class SndConnEvent {
|
||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is SwitchQueue -> {
|
||||
member?.profile?.profileViewName?.let {
|
||||
return when (phase) {
|
||||
SwitchPhase.Completed -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_completed_for_member), it)
|
||||
else -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_changing_for_member), it)
|
||||
}
|
||||
}
|
||||
when (phase) {
|
||||
SwitchPhase.Completed -> generalGetString(R.string.snd_conn_event_switch_queue_phase_completed)
|
||||
else -> generalGetString(R.string.snd_conn_event_switch_queue_phase_changing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SwitchPhase {
|
||||
@SerialName("started") Started,
|
||||
@SerialName("confirmed") Confirmed,
|
||||
@SerialName("completed") Completed
|
||||
}
|
||||
|
||||
sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
|
||||
object Day: ChatItemTTL()
|
||||
object Week: ChatItemTTL()
|
||||
|
||||
@@ -89,6 +89,7 @@ class AppPreferences(val context: Context) {
|
||||
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
|
||||
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
||||
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
|
||||
val privacyTransferImagesInline = mkBoolPreference(SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE, false)
|
||||
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
|
||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||
@@ -178,6 +179,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
|
||||
private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers"
|
||||
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
|
||||
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
|
||||
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
|
||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
|
||||
@@ -496,6 +498,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSwitchContact(contactId: Long) {
|
||||
return when (val r = sendCmd(CC.APISwitchContact(contactId))) {
|
||||
is CR.CmdOk -> {}
|
||||
else -> {
|
||||
apiErrorAlert("apiSwitchContact", generalGetString(R.string.connection_error), r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long) {
|
||||
return when (val r = sendCmd(CC.APISwitchGroupMember(groupId, groupMemberId))) {
|
||||
is CR.CmdOk -> {}
|
||||
else -> {
|
||||
apiErrorAlert("apiSwitchGroupMember", generalGetString(R.string.error_changing_address), r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiAddContact(): String? {
|
||||
val r = sendCmd(CC.AddContact())
|
||||
return when (r) {
|
||||
@@ -608,6 +628,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSetContactPrefs(contactId: Long, prefs: ChatPreferences): Contact? {
|
||||
val r = sendCmd(CC.ApiSetContactPrefs(contactId, prefs))
|
||||
if (r is CR.ContactPrefsUpdated) return r.toContact
|
||||
Log.e(TAG, "apiSetContactPrefs bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateUserAddress(): String? {
|
||||
val r = sendCmd(CC.CreateMyAddress())
|
||||
return when (r) {
|
||||
@@ -732,8 +759,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(fileId: Long): AChatItem? {
|
||||
val r = sendCmd(CC.ReceiveFile(fileId))
|
||||
suspend fun apiReceiveFile(fileId: Long, inline: Boolean): AChatItem? {
|
||||
val r = sendCmd(CC.ReceiveFile(fileId, inline))
|
||||
return when (r) {
|
||||
is CR.RcvFileAccepted -> r.chatItem
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
@@ -950,6 +977,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
chatModel.updateChatInfo(cInfo)
|
||||
}
|
||||
}
|
||||
is CR.ContactsMerged -> {
|
||||
if (chatModel.hasChat(r.mergedContact.id)) {
|
||||
if (chatModel.chatId.value == r.mergedContact.id) {
|
||||
chatModel.chatId.value = r.intoContact.id
|
||||
}
|
||||
chatModel.removeChat(r.mergedContact.id)
|
||||
}
|
||||
}
|
||||
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Connected())
|
||||
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Disconnected())
|
||||
is CR.ContactSubError -> processContactSubError(r.contact, r.chatError)
|
||||
@@ -1003,6 +1038,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(r.groupInfo), chatItems = listOf()))
|
||||
// TODO NtfManager.shared.notifyGroupInvitation
|
||||
}
|
||||
is CR.UserAcceptedGroupSent -> {
|
||||
chatModel.updateGroup(r.groupInfo)
|
||||
if (r.hostContact != null) {
|
||||
chatModel.dismissConnReqView(r.hostContact.activeConn.id)
|
||||
chatModel.removeChat(r.hostContact.activeConn.id)
|
||||
}
|
||||
}
|
||||
is CR.JoinedGroupMemberConnecting ->
|
||||
chatModel.upsertGroupMember(r.groupInfo, r.member)
|
||||
is CR.DeletedMemberUser -> // TODO update user member
|
||||
@@ -1103,7 +1145,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun receiveFile(fileId: Long) {
|
||||
val chatItem = apiReceiveFile(fileId)
|
||||
val inline = appPrefs.privacyTransferImagesInline.get()
|
||||
val chatItem = apiReceiveFile(fileId, inline)
|
||||
if (chatItem != null) {
|
||||
chatItemSimpleUpdate(chatItem)
|
||||
}
|
||||
@@ -1425,6 +1468,8 @@ sealed class CC {
|
||||
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
|
||||
class APIContactInfo(val contactId: Long): CC()
|
||||
class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APISwitchContact(val contactId: Long): CC()
|
||||
class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class AddContact: CC()
|
||||
class Connect(val connReq: String): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
|
||||
@@ -1434,6 +1479,7 @@ sealed class CC {
|
||||
class ApiParseMarkdown(val text: String): CC()
|
||||
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
||||
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
|
||||
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
|
||||
class CreateMyAddress: CC()
|
||||
class DeleteMyAddress: CC()
|
||||
class ShowMyAddress: CC()
|
||||
@@ -1449,7 +1495,7 @@ sealed class CC {
|
||||
class ApiRejectContact(val contactReqId: Long): CC()
|
||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||
class ReceiveFile(val fileId: Long): CC()
|
||||
class ReceiveFile(val fileId: Long, val inline: Boolean): CC()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is Console -> cmd
|
||||
@@ -1488,6 +1534,8 @@ sealed class CC {
|
||||
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
|
||||
is APIContactInfo -> "/_info @$contactId"
|
||||
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
|
||||
is APISwitchContact -> "/_switch @$contactId"
|
||||
is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId"
|
||||
is AddContact -> "/connect"
|
||||
is Connect -> "/connect $connReq"
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
|
||||
@@ -1497,6 +1545,7 @@ sealed class CC {
|
||||
is ApiParseMarkdown -> "/_parse $text"
|
||||
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
||||
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
|
||||
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
|
||||
is CreateMyAddress -> "/address"
|
||||
is DeleteMyAddress -> "/delete_address"
|
||||
is ShowMyAddress -> "/show_address"
|
||||
@@ -1512,7 +1561,7 @@ sealed class CC {
|
||||
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||
is ReceiveFile -> "/freceive $fileId"
|
||||
is ReceiveFile -> "/freceive $fileId inline=${onOff(inline)}"
|
||||
}
|
||||
|
||||
val cmdType: String get() = when (this) {
|
||||
@@ -1552,6 +1601,8 @@ sealed class CC {
|
||||
is APISetChatSettings -> "/apiSetChatSettings"
|
||||
is APIContactInfo -> "apiContactInfo"
|
||||
is APIGroupMemberInfo -> "apiGroupMemberInfo"
|
||||
is APISwitchContact -> "apiSwitchContact"
|
||||
is APISwitchGroupMember -> "apiSwitchGroupMember"
|
||||
is AddContact -> "addContact"
|
||||
is Connect -> "connect"
|
||||
is ApiDeleteChat -> "apiDeleteChat"
|
||||
@@ -1561,6 +1612,7 @@ sealed class CC {
|
||||
is ApiParseMarkdown -> "apiParseMarkdown"
|
||||
is ApiSetContactAlias -> "apiSetContactAlias"
|
||||
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
|
||||
is ApiSetContactPrefs -> "apiSetContactPrefs"
|
||||
is CreateMyAddress -> "createMyAddress"
|
||||
is DeleteMyAddress -> "deleteMyAddress"
|
||||
is ShowMyAddress -> "showMyAddress"
|
||||
@@ -1599,7 +1651,7 @@ sealed class CC {
|
||||
companion object {
|
||||
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
||||
|
||||
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ",")
|
||||
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ";")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1708,6 +1760,32 @@ data class ChatSettings(
|
||||
val enableNtfs: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChatPreferences(
|
||||
val voice: ChatPreference? = null
|
||||
) {
|
||||
companion object {
|
||||
val default = ChatPreferences(
|
||||
voice = ChatPreference(allow = PrefAllowed.NO)
|
||||
)
|
||||
val empty = ChatPreferences(
|
||||
voice = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChatPreference(
|
||||
val allow: PrefAllowed
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class PrefAllowed {
|
||||
@SerialName("always") ALWAYS,
|
||||
@SerialName("yes") YES,
|
||||
@SerialName("no") NO
|
||||
}
|
||||
|
||||
val json = Json {
|
||||
prettyPrint = true
|
||||
ignoreUnknownKeys = true
|
||||
@@ -1760,6 +1838,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR()
|
||||
@Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR()
|
||||
@Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val toConnection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val toContact: Contact): CR()
|
||||
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
|
||||
@Serializable @SerialName("userContactLink") class UserContactLink(val contactLink: UserContactLinkRec): CR()
|
||||
@Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val contactLink: UserContactLinkRec): CR()
|
||||
@@ -1787,7 +1866,7 @@ sealed class CR {
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo, val hostContact: Contact? = null): CR()
|
||||
@Serializable @SerialName("userDeletedMember") class UserDeletedMember(val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("leftMemberUser") class LeftMemberUser(val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("groupMembers") class GroupMembers(val group: Group): CR()
|
||||
@@ -1856,6 +1935,7 @@ sealed class CR {
|
||||
is UserProfileUpdated -> "userProfileUpdated"
|
||||
is ContactAliasUpdated -> "contactAliasUpdated"
|
||||
is ConnectionAliasUpdated -> "connectionAliasUpdated"
|
||||
is ContactPrefsUpdated -> "contactPrefsUpdated"
|
||||
is ParsedMarkdown -> "apiParsedMarkdown"
|
||||
is UserContactLink -> "userContactLink"
|
||||
is UserContactLinkUpdated -> "userContactLinkUpdated"
|
||||
@@ -1950,6 +2030,7 @@ sealed class CR {
|
||||
is UserProfileUpdated -> json.encodeToString(toProfile)
|
||||
is ContactAliasUpdated -> json.encodeToString(toContact)
|
||||
is ConnectionAliasUpdated -> json.encodeToString(toConnection)
|
||||
is ContactPrefsUpdated -> json.encodeToString(toContact)
|
||||
is ParsedMarkdown -> json.encodeToString(formattedText)
|
||||
is UserContactLink -> contactLink.responseDetails
|
||||
is UserContactLinkUpdated -> contactLink.responseDetails
|
||||
|
||||
@@ -65,6 +65,9 @@ fun ChatInfoView(
|
||||
},
|
||||
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
switchContactAddress = {
|
||||
showSwitchContactAddressAlert(chatModel, contact.contactId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +120,7 @@ fun ChatInfoLayout(
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
deleteContact: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
switchContactAddress: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -142,8 +146,12 @@ fun ChatInfoLayout(
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (connStats != null) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
if (developerTools) {
|
||||
SwitchAddressButton(switchContactAddress)
|
||||
SectionDivider()
|
||||
}
|
||||
if (connStats != null) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.network_status),
|
||||
@@ -162,8 +170,8 @@ fun ChatInfoLayout(
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
ClearChatButton(clearChat)
|
||||
SectionDivider()
|
||||
@@ -231,7 +239,9 @@ fun LocalAliasEditor(
|
||||
color = HighOrLowlight
|
||||
)
|
||||
},
|
||||
leadingIcon = if (leadingIcon) {{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }} else null,
|
||||
leadingIcon = if (leadingIcon) {
|
||||
{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }
|
||||
} else null,
|
||||
color = HighOrLowlight,
|
||||
focus = focus,
|
||||
textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center),
|
||||
@@ -311,6 +321,13 @@ fun SimplexServers(text: String, servers: List<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchAddressButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@@ -340,6 +357,21 @@ private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: C
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.switch_receiving_address_question),
|
||||
text = generalGetString(R.string.switch_receiving_address_desc),
|
||||
confirmText = generalGetString(R.string.switch_verb),
|
||||
onConfirm = {
|
||||
switchContactAddress(m, contactId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi {
|
||||
m.controller.apiSwitchContact(contactId)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
@@ -356,7 +388,9 @@ fun PreviewChatInfoLayout() {
|
||||
connStats = null,
|
||||
onLocalAliasChanged = {},
|
||||
customUserProfile = null,
|
||||
deleteContact = {}, clearChat = {}
|
||||
deleteContact = {},
|
||||
clearChat = {},
|
||||
switchContactAddress = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,18 +74,8 @@ fun GroupLinkLayout(
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (groupLink == null) {
|
||||
Text(
|
||||
stringResource(R.string.if_you_later_delete_link_you_wont_lose_members),
|
||||
Modifier.padding(bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, click = createLink)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.if_you_delete_group_link_you_wont_lose_members),
|
||||
Modifier.padding(bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
|
||||
+21
-8
@@ -24,6 +24,7 @@ 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.chat.SwitchAddressButton
|
||||
import chat.simplex.app.views.chatlist.openChat
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
@@ -81,6 +82,9 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
switchMemberAddress = {
|
||||
switchMemberAddress(chatModel, groupInfo, member)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -113,6 +117,7 @@ fun GroupMemberInfoLayout(
|
||||
openDirectChat: () -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -154,12 +159,15 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (connStats != null) {
|
||||
val rcvServers = connStats.rcvServers
|
||||
val sndServers = connStats.sndServers
|
||||
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
if (developerTools) {
|
||||
SwitchAddressButton(switchMemberAddress)
|
||||
SectionDivider()
|
||||
}
|
||||
if (connStats != null) {
|
||||
val rcvServers = connStats.rcvServers
|
||||
val sndServers = connStats.sndServers
|
||||
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
|
||||
if (rcvServers != null && rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
|
||||
if (sndServers != null && sndServers.isNotEmpty()) {
|
||||
@@ -170,9 +178,9 @@ fun GroupMemberInfoLayout(
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
SectionView {
|
||||
@@ -280,6 +288,10 @@ private fun updateMemberRoleDialog(
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchMemberAddress(m: ChatModel, groupInfo: GroupInfo, member: GroupMember) = withApi {
|
||||
m.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewGroupMemberInfoLayout() {
|
||||
@@ -292,7 +304,8 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
developerTools = false,
|
||||
openDirectChat = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {}
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
|
||||
@Composable
|
||||
fun CIGroupEventView(ci: ChatItem) {
|
||||
fun CIEventView(ci: ChatItem) {
|
||||
fun withGroupEventStyle(builder: AnnotatedString.Builder, text: String) {
|
||||
return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) }
|
||||
}
|
||||
@@ -50,9 +50,9 @@ fun CIGroupEventView(ci: ChatItem) {
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun CIGroupEventViewPreview() {
|
||||
fun CIEventViewPreview() {
|
||||
SimpleXTheme {
|
||||
CIGroupEventView(
|
||||
CIEventView(
|
||||
ChatItem.getGroupEventSample()
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -158,8 +157,10 @@ fun ChatItemView(
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
|
||||
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)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
@@ -25,6 +26,7 @@ import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import com.google.accompanist.pager.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
interface ImageGalleryProvider {
|
||||
val initialIndex: Int
|
||||
@@ -88,6 +90,8 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var translationX by remember { mutableStateOf(0f) }
|
||||
var translationY by remember { mutableStateOf(0f) }
|
||||
var viewWidth by remember { mutableStateOf(0) }
|
||||
var allowTranslate by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(settledCurrentPage) {
|
||||
scale = 1f
|
||||
translationX = 0f
|
||||
@@ -113,6 +117,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
viewWidth = it.size.width
|
||||
}
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
@@ -121,12 +128,14 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures(
|
||||
{ allowTranslate },
|
||||
onGesture = { _, pan, gestureZoom, _ ->
|
||||
scale = (scale * gestureZoom).coerceIn(1f, 20f)
|
||||
if (scale > 1) {
|
||||
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
|
||||
if (scale > 1 && allowTranslate) {
|
||||
translationX += pan.x * scale
|
||||
translationY += pan.y * scale
|
||||
} else {
|
||||
} else if (allowTranslate) {
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
|
||||
+1
-2
@@ -11,7 +11,6 @@ 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.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -535,7 +534,7 @@ fun ChatListNavLinkLayout(
|
||||
showMenu: MutableState<Boolean>,
|
||||
stopped: Boolean
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp)
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!stopped) modifier = modifier.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
Surface(modifier) {
|
||||
Row(
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -122,7 +123,10 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
.weight(1F)
|
||||
) {
|
||||
chatPreviewTitle()
|
||||
chatPreviewText(chatModelIncognito)
|
||||
val height = with(LocalDensity.current) { 46.sp.toDp() }
|
||||
Row(Modifier.heightIn(min = height)) {
|
||||
chatPreviewText(chatModelIncognito)
|
||||
}
|
||||
}
|
||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
|
||||
|
||||
|
||||
+4
-1
@@ -8,9 +8,11 @@ import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
@@ -34,7 +36,8 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) {
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
Text(contactConnection.description, maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
val height = with(LocalDensity.current) { 46.sp.toDp() }
|
||||
Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
}
|
||||
val ts = getTimestampText(contactConnection.updatedAt)
|
||||
Column(
|
||||
|
||||
+4
-1
@@ -5,10 +5,12 @@ import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
@@ -31,7 +33,8 @@ fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.Con
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary
|
||||
)
|
||||
Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
val height = with(LocalDensity.current) { 46.sp.toDp() }
|
||||
Text(stringResource(R.string.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
}
|
||||
val ts = getTimestampText(contactRequest.contactRequest.updatedAt)
|
||||
Column(
|
||||
|
||||
@@ -214,13 +214,14 @@ fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit)
|
||||
}
|
||||
|
||||
suspend fun PointerInputScope.detectTransformGestures(
|
||||
allowIntercept: () -> Boolean,
|
||||
panZoomLock: Boolean = false,
|
||||
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
|
||||
) {
|
||||
var zoom = 1f
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
var rotation = 0f
|
||||
var zoom = 1f
|
||||
var pan = Offset.Zero
|
||||
var pastTouchSlop = false
|
||||
val touchSlop = viewConfiguration.touchSlop
|
||||
@@ -264,7 +265,7 @@ suspend fun PointerInputScope.detectTransformGestures(
|
||||
onGesture(centroid, panChange, zoomChange, effectiveRotation)
|
||||
}
|
||||
event.changes.fastForEach {
|
||||
if (it.positionChanged() && zoomChange != 1f) {
|
||||
if (it.positionChanged() && zoom != 1f && allowIntercept()) {
|
||||
it.consume()
|
||||
}
|
||||
}
|
||||
|
||||
+13
-18
@@ -46,10 +46,7 @@ fun ContactConnectionInfoView(
|
||||
}
|
||||
ContactConnectionInfoLayout(
|
||||
connReq = connReqInvitation,
|
||||
contactConnection.localAlias,
|
||||
contactConnection.initiated,
|
||||
contactConnection.viaContactUri,
|
||||
contactConnection.incognito,
|
||||
contactConnection,
|
||||
focusAlias,
|
||||
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
|
||||
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
|
||||
@@ -71,10 +68,7 @@ fun ContactConnectionInfoView(
|
||||
@Composable
|
||||
private fun ContactConnectionInfoLayout(
|
||||
connReq: String?,
|
||||
localAlias: String,
|
||||
connectionInitiated: Boolean,
|
||||
connectionViaContactUri: Boolean,
|
||||
connectionIncognito: Boolean,
|
||||
contactConnection: PendingContactConnection,
|
||||
focusAlias: Boolean,
|
||||
deleteConnection: () -> Unit,
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
@@ -86,23 +80,27 @@ private fun ContactConnectionInfoLayout(
|
||||
) {
|
||||
AppBarTitle(
|
||||
stringResource(
|
||||
if (connectionInitiated) R.string.you_invited_your_contact
|
||||
if (contactConnection.initiated) R.string.you_invited_your_contact
|
||||
else R.string.you_accepted_connection
|
||||
)
|
||||
)
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING)) {
|
||||
LocalAliasEditor(localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
|
||||
if (contactConnection.groupLinkId == null) {
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING)) {
|
||||
LocalAliasEditor(contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(
|
||||
if (connectionViaContactUri) R.string.you_will_be_connected_when_your_connection_request_is_accepted
|
||||
if (contactConnection.viaContactUri)
|
||||
if (contactConnection.groupLinkId != null) R.string.you_will_be_connected_when_group_host_device_is_online
|
||||
else R.string.you_will_be_connected_when_your_connection_request_is_accepted
|
||||
else R.string.you_will_be_connected_when_your_contacts_device_is_online
|
||||
),
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
|
||||
)
|
||||
SectionView {
|
||||
if (!connReq.isNullOrEmpty() && connectionInitiated) {
|
||||
ShowQrButton(connectionIncognito, showQr)
|
||||
if (!connReq.isNullOrEmpty() && contactConnection.initiated) {
|
||||
ShowQrButton(contactConnection.incognito, showQr)
|
||||
SectionDivider()
|
||||
}
|
||||
DeleteButton(deleteConnection)
|
||||
@@ -148,11 +146,8 @@ private fun setContactAlias(contactConnection: PendingContactConnection, localAl
|
||||
private fun PreviewContactConnectionInfoView() {
|
||||
SimpleXTheme {
|
||||
ContactConnectionInfoLayout(
|
||||
localAlias = "",
|
||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
connectionInitiated = true,
|
||||
connectionViaContactUri = true,
|
||||
connectionIncognito = false,
|
||||
PendingContactConnection.getSampleData(),
|
||||
focusAlias = false,
|
||||
deleteConnection = {},
|
||||
onLocalAliasChanged = {},
|
||||
|
||||
+5
-5
@@ -4,19 +4,15 @@ import SectionDivider
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material.icons.outlined.TravelExplore
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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.model.ChatModel
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
@@ -33,6 +29,10 @@ fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
SectionView(stringResource(R.string.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SectionDivider()
|
||||
if (chatModel.controller.appPrefs.developerTools.get()) {
|
||||
SettingsPreferenceItem(Icons.Outlined.ImageAspectRatio, stringResource(R.string.transfer_images_faster), chatModel.controller.appPrefs.privacyTransferImagesInline)
|
||||
SectionDivider()
|
||||
}
|
||||
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,18 +84,8 @@ fun UserAddressLayout(
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (userAddress == null) {
|
||||
Text(
|
||||
stringResource(R.string.if_you_later_delete_address_you_wont_lose_contacts),
|
||||
Modifier.align(Alignment.Start).padding(bottom = 24.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
SimpleButton(stringResource(R.string.create_address), icon = Icons.Outlined.QrCode, click = createAddress)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.if_you_delete_address_you_wont_lose_contacts),
|
||||
Modifier.align(Alignment.Start).padding(bottom = 24.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
QRCode(userAddress.connReqContact, Modifier.aspectRatio(1f))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- Server info - ChatModel.kt -->
|
||||
<string name="server_connected">Verbunden</string>
|
||||
<string name="server_error">Fehler</string>
|
||||
<string name="server_connecting">verbinde</string>
|
||||
<string name="server_connecting">Verbinde</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
|
||||
@@ -26,16 +26,18 @@
|
||||
<string name="invalid_message_format">Unzulässiges Nachrichtenformat</string>
|
||||
|
||||
<!-- PendingContactConnection - ChatModel.kt -->
|
||||
<string name="connection_local_display_name">Verbindung <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">Verbindung hergestellt</string>
|
||||
<string name="display_name_invited_to_connect">Für eine Verbindung eingeladen</string>
|
||||
<string name="display_name_connecting">verbinde …</string>
|
||||
<string name="description_you_shared_one_time_link">Sie haben einen Einmal-Link geteilt</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">Sie haben Inkognito einen Einmal-Link geteilt</string>
|
||||
<string name="connection_local_display_name">verbindung <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">verbindung hergestellt</string>
|
||||
<string name="display_name_invited_to_connect">für eine Verbindung eingeladen</string>
|
||||
<string name="display_name_connecting">verbinde…</string>
|
||||
<string name="description_you_shared_one_time_link">sie haben einen Einmal-Link geteilt</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">sie haben Inkognito einen Einmal-Link geteilt</string>
|
||||
<string name="description_via_group_link">***via group link</string>
|
||||
<string name="description_via_group_link_incognito">***incognito via group link</string>
|
||||
<string name="description_via_contact_address_link">über einen Kontaktadressen Link</string>
|
||||
<string name="description_via_contact_address_link_incognito">Inkognito über einen Kontaktadressen Link</string>
|
||||
<string name="description_via_contact_address_link_incognito">inkognito über einen Kontaktadressen Link</string>
|
||||
<string name="description_via_one_time_link">über einen Einmal-Link</string>
|
||||
<string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string>
|
||||
<string name="description_via_one_time_link_incognito">inkognito über einen Einmal-Link</string>
|
||||
|
||||
<!-- SimpleXAPI.kt -->
|
||||
<string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string>
|
||||
@@ -63,8 +65,9 @@
|
||||
<string name="sender_may_have_deleted_the_connection_request">Der Absender hat möglicherweise die Verbindungsanfrage gelöscht.</string>
|
||||
<string name="error_deleting_contact">Fehler beim Löschen des Kontakts</string>
|
||||
<string name="error_deleting_group">Fehler beim Löschen der Gruppe</string>
|
||||
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontakt-Anfrage</string>
|
||||
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontaktanfrage</string>
|
||||
<string name="error_deleting_pending_contact_connection">Fehler beim Löschen der anstehenden Kontaktaufnahme</string>
|
||||
<string name="error_changing_address">*** Error changing address</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string>
|
||||
@@ -109,9 +112,9 @@
|
||||
<string name="notification_preview_mode_contact_desc">Nur Kontakt anzeigen</string>
|
||||
<string name="notification_display_mode_hidden_desc">Kontakt und Nachricht verbergen</string>
|
||||
<string name="notification_preview_somebody">Kontakt verbergen:</string>
|
||||
<string name="notification_preview_new_message">neue Nachricht</string>
|
||||
<string name="notification_new_contact_request">New contact request</string>
|
||||
<string name="notification_contact_connected">Сonnected</string>
|
||||
<string name="notification_preview_new_message">Neue Nachricht</string>
|
||||
<string name="notification_new_contact_request">Neue Kontaktanfrage</string>
|
||||
<string name="notification_contact_connected">Verbunden</string>
|
||||
|
||||
<!-- local authentication notice - SimpleXAPI.kt -->
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Sperre</string>
|
||||
@@ -144,16 +147,16 @@
|
||||
<string name="edit_verb">Bearbeiten</string>
|
||||
<string name="delete_verb">Löschen</string>
|
||||
<string name="delete_message__question">Die Nachricht löschen?</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="for_me_only">Nur für mich</string>
|
||||
<string name="for_everybody">Für alle</string>
|
||||
|
||||
<!-- CIMetaView.kt -->
|
||||
<string name="icon_descr_edited">bearbeitet</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">gesendet</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">nicht autorisiertes Senden</string>
|
||||
<string name="icon_descr_edited">Bearbeitet</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">Gesendet</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">Nicht autorisiertes Senden</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">Senden fehlgeschlagen</string>
|
||||
<string name="icon_descr_received_msg_status_unread">ungelesen</string>
|
||||
<string name="icon_descr_received_msg_status_unread">Ungelesen</string>
|
||||
|
||||
<!-- ChatListView.kt -->
|
||||
<string name="personal_welcome">Willkommen <xliff:g>%1$s</xliff:g>!</string>
|
||||
@@ -162,9 +165,9 @@
|
||||
<string name="your_chats">Meine Chats</string>
|
||||
<string name="contact_connection_pending">verbinde …</string>
|
||||
<string name="group_preview_you_are_invited">Sie sind zu der Gruppe eingeladen</string>
|
||||
<string name="group_preview_join_as">beitreten als %s</string>
|
||||
<string name="group_preview_join_as">Beitreten als %s</string>
|
||||
<string name="group_connection_pending">verbinde …</string>
|
||||
<string name="tap_to_start_new_chat">Tippen Sie um einen neuen Chat zu starten</string>
|
||||
<string name="tap_to_start_new_chat">Tippen Sie, um einen neuen Chat zu starten</string>
|
||||
<string name="chat_with_developers">Chatten Sie mit den Entwicklern</string>
|
||||
<string name="you_have_no_chats">Sie haben keine Chats</string>
|
||||
|
||||
@@ -184,7 +187,7 @@
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Bild</string>
|
||||
<string name="icon_descr_waiting_for_image">Warten auf ein Bild</string>
|
||||
<string name="icon_descr_asked_to_receive">Um Empfang des Bildes gebeten</string>
|
||||
<string name="icon_descr_asked_to_receive">Es wird um den Empfang eines Bildes gebeten</string>
|
||||
<string name="icon_descr_image_snd_complete">Bild gesendet</string>
|
||||
<string name="waiting_for_image">Warten auf ein Bild</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">Das Bild wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
@@ -213,6 +216,8 @@
|
||||
<string name="icon_descr_server_status_disconnected">Getrennt</string>
|
||||
<string name="icon_descr_server_status_error">Fehler</string>
|
||||
<string name="icon_descr_server_status_pending">Ausstehend</string>
|
||||
<string name="switch_receiving_address_question">*** Switch receiving address?</string>
|
||||
<string name="switch_receiving_address_desc">*** This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Nachricht senden</string>
|
||||
@@ -222,7 +227,7 @@
|
||||
<string name="cancel_verb">Abbrechen</string>
|
||||
<string name="confirm_verb">Bestätigen</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">keine Details</string>
|
||||
<string name="no_details">Keine Details</string>
|
||||
<string name="add_contact">Kontakt hinzufügen</string>
|
||||
<string name="copied">In die Zwischenablage kopiert</string>
|
||||
|
||||
@@ -234,7 +239,7 @@
|
||||
<string name="create_group">Geheime Gruppe erstellen</string>
|
||||
<string name="to_share_with_your_contact">(zum Teilen mit Ihrem Kontakt)</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(Scannen oder Einfügen aus der Zwischenablage)</string>
|
||||
<string name="only_stored_on_members_devices">(wird nur von Gruppenmitgliedern gespeichert)</string>
|
||||
<string name="only_stored_on_members_devices">(Wird nur von Gruppenmitgliedern gespeichert)</string>
|
||||
|
||||
<!-- GetImageView -->
|
||||
<string name="toast_permission_denied">Berechtigung verweigert!</string>
|
||||
@@ -247,7 +252,7 @@
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Sie können sich <font color="#0088ff">mit <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.</string>
|
||||
<string name="to_start_a_new_chat_help_header">Um einen neuen Chat zu starten</string>
|
||||
<string name="chat_help_tap_button">Schaltfläche antippen</string>
|
||||
<string name="above_then_preposition_continuation">über, dann:</string>
|
||||
<string name="above_then_preposition_continuation">Danach die gewünschte Aktion auswählen:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Neuen Kontakt hinzufügen</b>: Um Ihren Einmal-QR-Code für Ihren Kontakt zu erstellen.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR-Code scannen</b>: Um sich mit Ihrem Kontakt zu verbinden, der Ihnen seinen QR-Code zeigt.</string>
|
||||
<string name="to_connect_via_link_title">Über Link verbinden</string>
|
||||
@@ -271,7 +276,7 @@
|
||||
<string name="delete_contact_menu_action">Kontakt löschen</string>
|
||||
<string name="delete_group_menu_action">Gruppe löschen</string>
|
||||
<string name="mark_read">Als gelesen markieren</string>
|
||||
<string name="mark_unread">Mark unread</string>
|
||||
<string name="mark_unread">Als ungelesen markieren</string>
|
||||
<string name="set_contact_name">Kontaktname festlegen</string>
|
||||
|
||||
<!-- Actions - ChatListNavLinkView.kt -->
|
||||
@@ -297,7 +302,7 @@
|
||||
<string name="image_descr_profile_image">Profilbild</string>
|
||||
|
||||
<!-- Content Descriptions -->
|
||||
<string name="icon_descr_close_button">Schließen Schaltfläche</string>
|
||||
<string name="icon_descr_close_button">Schließen der Schaltfläche</string>
|
||||
<string name="image_descr_link_preview">Vorschaubild verlinken</string>
|
||||
<string name="icon_descr_cancel_link_preview">Link Vorschau abbrechen</string>
|
||||
<string name="icon_descr_settings">Einstellungen</string>
|
||||
@@ -318,6 +323,7 @@
|
||||
<string name="invalid_contact_link">Ungültiger Link!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Dieser Link ist kein gültiger Verbindungslink!</string>
|
||||
<string name="connection_request_sent">Verbindungsanfrage gesendet!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">***You will be connected to group when the group host\'s device is online, please wait or check later!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Zeigen Sie Ihrem Kontakt den QR-Code aus der App zum Scannen.</string>
|
||||
@@ -393,17 +399,15 @@
|
||||
<string name="create_address">Adresse erstellen</string>
|
||||
<string name="delete_address__question">Adresse löschen?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Alle Ihre Kontakte bleiben verbunden.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen – Jeder kann sich mit Ihnen verbinden.</string>
|
||||
<string name="if_you_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse löschen, werden Sie Ihre Kontakte nicht verlieren.</string>
|
||||
<string name="if_you_later_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse später löschen, werden Sie Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen – Jeder kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string>
|
||||
<string name="share_link">Link teilen</string>
|
||||
<string name="delete_address">Adresse löschen</string>
|
||||
|
||||
<!-- AcceptRequestsView.kt -->
|
||||
<string name="contact_requests">***Contact requests</string>
|
||||
<string name="accept_requests">***Accept requests</string>
|
||||
<string name="accept_automatically">***Automatically</string>
|
||||
<string name="section_title_welcome_message">***WELCOME MESSAGE</string>
|
||||
<string name="contact_requests">Kontaktanfragen</string>
|
||||
<string name="accept_requests">Anfragen annehmen</string>
|
||||
<string name="accept_automatically">Automatisch</string>
|
||||
<string name="section_title_welcome_message">Begrüßungsmeldung</string>
|
||||
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Angezeigter Name:</string>
|
||||
@@ -454,8 +458,8 @@
|
||||
<string name="callstate_received_answer">Antwort erhalten…</string>
|
||||
<string name="callstate_received_confirmation">Bestätigung erhalten…</string>
|
||||
<string name="callstate_connecting">Verbindung wird hergestellt…</string>
|
||||
<string name="callstate_connected">verbunden</string>
|
||||
<string name="callstate_ended">beendet</string>
|
||||
<string name="callstate_connected">Verbunden</string>
|
||||
<string name="callstate_ended">Beendet</string>
|
||||
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">Die nächste Generation von privatem Messaging</string>
|
||||
@@ -549,10 +553,11 @@
|
||||
<string name="privacy_and_security">Datenschutz & Sicherheit</string>
|
||||
<string name="your_privacy">Meine Privatsphäre</string>
|
||||
<string name="auto_accept_images">Bilder automatisch akzeptieren</string>
|
||||
<string name="transfer_images_faster">*** Transfer images faster (BETA)</string>
|
||||
<string name="send_link_previews">Link-Vorschau senden</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">DU</string>
|
||||
<string name="settings_section_title_you">MEINE DATEN</string>
|
||||
<string name="settings_section_title_settings">EINSTELLUNGEN</string>
|
||||
<string name="settings_section_title_help">HILFE</string>
|
||||
<string name="settings_section_title_develop">ENTWICKLUNG</string>
|
||||
@@ -570,8 +575,8 @@
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Meine Chat-Datenbank</string>
|
||||
<string name="run_chat_section">CHAT STARTEN</string>
|
||||
<string name="chat_is_running">Chat läuft</string>
|
||||
<string name="chat_is_stopped">Chat ist beendet</string>
|
||||
<string name="chat_is_running">Der Chat läuft</string>
|
||||
<string name="chat_is_stopped">Der Chat ist beendet</string>
|
||||
<string name="chat_database_section">CHAT-DATENBANK</string>
|
||||
<string name="database_passphrase">Datenbank-Passwort</string>
|
||||
<string name="export_database">Datenbank exportieren</string>
|
||||
@@ -581,7 +586,7 @@
|
||||
<string name="delete_database">Datenbank löschen</string>
|
||||
<string name="error_starting_chat">Fehler beim Starten des Chats</string>
|
||||
<string name="stop_chat_question">Chat beenden?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Sie können keine Nachrichten empfangen oder senden, solange der Chat angehalten ist.</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Solange der Chat angehalten ist, können Sie keine Nachrichten empfangen oder senden.</string>
|
||||
<string name="stop_chat_confirmation">Beenden</string>
|
||||
<string name="set_password_to_export">Passwort zum Exportieren festlegen</string>
|
||||
<string name="set_password_to_export_desc">Die Datenbank ist mit einem zufälligen Passwort verschlüsselt. Bitte ändern Sie es vor dem Exportieren.</string>
|
||||
@@ -603,17 +608,17 @@
|
||||
<string name="data_section">DATEN</string>
|
||||
<string name="delete_files_and_media">Dateien \& Medien löschen</string>
|
||||
<string name="delete_files_and_media_question">Dateien und Medien löschen?</string>
|
||||
<string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden - alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string>
|
||||
<string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string>
|
||||
<string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string>
|
||||
<string name="total_files_count_and_size">%d Datei(en) mit einem Gesamtspeicherverbrauch von %s</string>
|
||||
<string name="chat_item_ttl_none">nie</string>
|
||||
<string name="chat_item_ttl_day">1 Tag</string>
|
||||
<string name="chat_item_ttl_week">1 Woche</string>
|
||||
<string name="chat_item_ttl_month">1 Monat</string>
|
||||
<string name="chat_item_ttl_day">täglich</string>
|
||||
<string name="chat_item_ttl_week">wöchentlich</string>
|
||||
<string name="chat_item_ttl_month">monatlich</string>
|
||||
<string name="chat_item_ttl_seconds">%s Sekunde(n)</string>
|
||||
<string name="delete_messages_after">Löschen der Nachrichten nach</string>
|
||||
<string name="delete_messages_after">Löschen der Nachrichten</string>
|
||||
<string name="enable_automatic_deletion_question">Automatisches Löschen von Nachrichten aktivieren?</string>
|
||||
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, werden gelöscht. Dies kann mehrere Minuten dauern.</string>
|
||||
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dies kann mehrere Minuten dauern.</string>
|
||||
<string name="delete_messages">Nachrichten löschen</string>
|
||||
<string name="error_changing_message_deletion">Fehler beim Ändern der Einstellung</string>
|
||||
|
||||
@@ -688,10 +693,10 @@
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Sie sind zu einer Gruppe eingeladen worden. Treten Sie bei, um sich mit Gruppenmitgliedern zu verbinden.</string>
|
||||
<string name="join_group_button">Beitreten</string>
|
||||
<string name="join_group_incognito_button">Inkognito beitreten</string>
|
||||
<string name="joining_group">Gruppe beitreten</string>
|
||||
<string name="joining_group">Der Gruppe beitreten</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden.</string>
|
||||
<string name="leave_group_button">Verlassen</string>
|
||||
<string name="leave_group_question">Gruppe verlassen?</string>
|
||||
<string name="leave_group_question">Die Gruppe verlassen?</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</string>
|
||||
<string name="icon_descr_add_members">Mitglieder einladen</string>
|
||||
<string name="icon_descr_group_inactive">Gruppe inaktiv</string>
|
||||
@@ -712,21 +717,29 @@
|
||||
<string name="group_invitation_expired">Die Gruppeneinladung ist abgelaufen</string>
|
||||
|
||||
<!-- Group event chat items -->
|
||||
<string name="rcv_group_event_member_added">***sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> eingeladen.</string>
|
||||
<string name="rcv_group_event_member_connected">verbunden</string>
|
||||
<string name="rcv_group_event_member_added">hat <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> eingeladen.</string>
|
||||
<string name="rcv_group_event_member_connected">beigetreten</string>
|
||||
<string name="rcv_group_event_member_left">verlassen</string>
|
||||
<string name="rcv_group_event_changed_member_role">***changed role of %s to %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">***changed your role to %s</string>
|
||||
<string name="rcv_group_event_member_deleted">entfernte <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g>.</string>
|
||||
<string name="rcv_group_event_user_deleted">hat Sie entfernt</string>
|
||||
<string name="rcv_group_event_group_deleted">gruppe gelöscht</string>
|
||||
<string name="rcv_group_event_updated_group_profile">aktualisiertes gruppenprofil</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">***invited via your group link</string>
|
||||
<string name="snd_group_event_changed_member_role">***you changed role of %s to %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">***you changed role for yourself to %s</string>
|
||||
<string name="snd_group_event_member_deleted">sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> entfernt.</string>
|
||||
<string name="snd_group_event_user_left">sie haben verlassen</string>
|
||||
<string name="snd_group_event_group_profile_updated">gruppenprofil aktualisiert</string>
|
||||
<string name="rcv_group_event_changed_member_role">änderte die Rolle von %s auf %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">änderte Ihre Rolle auf %s</string>
|
||||
<string name="rcv_group_event_member_deleted">entfernte <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> aus der Gruppe.</string>
|
||||
<string name="rcv_group_event_user_deleted">hat Sie aus der Gruppe entfernt</string>
|
||||
<string name="rcv_group_event_group_deleted">Gruppe gelöscht</string>
|
||||
<string name="rcv_group_event_updated_group_profile">Aktualisiertes Gruppenprofil</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">wurde über Ihren Gruppen-Link eingeladen</string>
|
||||
<string name="snd_group_event_changed_member_role">Sie haben die Rolle von %s auf %s geändert</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">Sie haben Ihre eigene Rolle auf %s geändert</string>
|
||||
<string name="snd_group_event_member_deleted">entfernt <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> aus der Gruppe.</string>
|
||||
<string name="snd_group_event_user_left">hat die Gruppe verlassen</string>
|
||||
<string name="snd_group_event_group_profile_updated">Gruppenprofil aktualisiert</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">*** changed address for you</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">*** changing address…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">*** you changed address for %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">*** changing address for %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">*** you changed address</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">*** changing address…</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_member">Mitglied</string>
|
||||
@@ -734,16 +747,16 @@
|
||||
<string name="group_member_role_owner">Eigentümer</string>
|
||||
|
||||
<!-- GroupMemberStatus -->
|
||||
<string name="group_member_status_removed">entfernt</string>
|
||||
<string name="group_member_status_left">verlassen</string>
|
||||
<string name="group_member_status_removed">Entfernt</string>
|
||||
<string name="group_member_status_left">Verlassen</string>
|
||||
<string name="group_member_status_group_deleted">Gruppe gelöscht</string>
|
||||
<string name="group_member_status_invited">eingeladen</string>
|
||||
<string name="group_member_status_invited">Eingeladen</string>
|
||||
<string name="group_member_status_introduced">Verbindung (erstellt)</string>
|
||||
<string name="group_member_status_intro_invitation">Verbindung (eingeladen)</string>
|
||||
<string name="group_member_status_accepted">Verbindung (akzeptiert)</string>
|
||||
<string name="group_member_status_announced">Verbindung (angekündigt)</string>
|
||||
<string name="group_member_status_connected">verbunden</string>
|
||||
<string name="group_member_status_complete">vollständig</string>
|
||||
<string name="group_member_status_connected">Verbunden</string>
|
||||
<string name="group_member_status_complete">Vollständig</string>
|
||||
<string name="group_member_status_creator">Ersteller</string>
|
||||
|
||||
<string name="group_member_status_connecting">verbinden</string>
|
||||
@@ -770,16 +783,14 @@
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="button_leave_group">Gruppe verlassen</string>
|
||||
<string name="button_edit_group_profile">Gruppenprofil bearbeiten</string>
|
||||
<string name="group_link">***Group link</string>
|
||||
<string name="button_create_group_link">***Create link</string>
|
||||
<string name="delete_link_question">***Delete link?</string>
|
||||
<string name="delete_link">***Delete link</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">***You can share a link or a QR code - anybody will be able to join the group.</string>
|
||||
<string name="if_you_later_delete_link_you_wont_lose_members">***If you later delete it - you won\'t lose members of the group connected via the link.</string>
|
||||
<string name="if_you_delete_group_link_you_wont_lose_members">***If you delete it - you won\'t lose members of the group connected via this link.</string>
|
||||
<string name="all_group_members_will_remain_connected">***All group members will remain connected.</string>
|
||||
<string name="error_creating_link_for_group">***Error creating group link</string>
|
||||
<string name="error_deleting_link_for_group">***Error deleting group link</string>
|
||||
<string name="group_link">Gruppen-Link</string>
|
||||
<string name="button_create_group_link">Link erzeugen</string>
|
||||
<string name="delete_link_question">Link löschen?</string>
|
||||
<string name="delete_link">Link löschen</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.</string>
|
||||
<string name="all_group_members_will_remain_connected">Alle Gruppenmitglieder bleiben verbunden.</string>
|
||||
<string name="error_creating_link_for_group">Fehler beim Erzeugen des Gruppen-Links</string>
|
||||
<string name="error_deleting_link_for_group">Fehler beim Löschen des Gruppen-Links</string>
|
||||
|
||||
<!-- For Console chat info section -->
|
||||
<string name="section_title_for_console">FÜR KONSOLE</string>
|
||||
@@ -795,6 +806,7 @@
|
||||
<string name="role_in_group">Rolle</string>
|
||||
<string name="change_role">Rolle ändern</string>
|
||||
<string name="change_verb">Ändern</string>
|
||||
<string name="switch_verb">***Switch</string>
|
||||
<string name="change_member_role_question">Die Mitgliederrolle ändern?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
|
||||
@@ -810,6 +822,7 @@
|
||||
<string name="receiving_via">Empfangen über</string>
|
||||
<string name="sending_via">Senden über</string>
|
||||
<string name="network_status">Netzwerkstatus</string>
|
||||
<string name="switch_receiving_address">***Switch receiving address (BETA)</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Geheime Gruppe erstellen</string>
|
||||
@@ -817,7 +830,7 @@
|
||||
<string name="group_display_name_field">Anzeigename der Gruppe:</string>
|
||||
<string name="group_full_name_field">Vollständiger Gruppenname:</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Der Inkognito-Modus wird hier nicht unterstützt - Ihr Hauptprofil wird an die Gruppenmitglieder gesendet</string>
|
||||
<string name="group_main_profile_sent">Ihr Chat-Profil wird an Gruppenmitglieder gesendet</string>
|
||||
<string name="group_main_profile_sent">Ihr Chat-Profil wird an die Gruppenmitglieder gesendet</string>
|
||||
|
||||
|
||||
<!-- GroupProfileView.kt -->
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
<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_group_link">через ссылку группы</string>
|
||||
<string name="description_via_group_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>
|
||||
@@ -61,10 +63,11 @@
|
||||
<string name="connection_error_auth_desc">Возможно, ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите ваш контакт создать еще одну ссылку и проверьте ваше соединение с сетью.</string>
|
||||
<string name="error_accepting_contact_request">Ошибка при принятии запроса на соединение</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">Отправитель мог удалить запрос на соединение.</string>
|
||||
<string name="error_deleting_contact">Ошибка удаления контакта</string>
|
||||
<string name="error_deleting_contact">Ошибка при удалении контакта</string>
|
||||
<string name="error_deleting_group">Ошибка удаления группы</string>
|
||||
<string name="error_deleting_contact_request">Ошибка удаления запроса</string>
|
||||
<string name="error_deleting_pending_contact_connection">Ошибка удаления ожидаемого соединения</string>
|
||||
<string name="error_changing_address">Ошибка при изменении адреса</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Мгновенные уведомления</string>
|
||||
@@ -213,6 +216,8 @@
|
||||
<string name="icon_descr_server_status_disconnected">Соединение с сервером не установлено</string>
|
||||
<string name="icon_descr_server_status_error">Ошибка соединения с сервером</string>
|
||||
<string name="icon_descr_server_status_pending">Ожидается соединение с сервером</string>
|
||||
<string name="switch_receiving_address_question">Переключить адрес получения?</string>
|
||||
<string name="switch_receiving_address_desc">Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Отправить сообщение</string>
|
||||
@@ -318,8 +323,9 @@
|
||||
<string name="invalid_contact_link">Неверная ссылка!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Эта ссылка не является ссылкой-приглашением!</string>
|
||||
<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="you_will_be_connected_when_group_host_device_is_online">Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!</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="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>
|
||||
@@ -390,9 +396,7 @@
|
||||
<string name="create_address">Создать адрес</string>
|
||||
<string name="delete_address__question">Удалить адрес?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Все контакты, которые соединились через этот адрес, сохранятся.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать адрес как ссылку или как QR код - через него можно с вами соединиться.</string>
|
||||
<string name="if_you_later_delete_address_you_wont_lose_contacts">Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
|
||||
<string name="if_you_delete_address_you_wont_lose_contacts">Вы можете удалить адрес, сохранив контакты, которые через него соединились.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
|
||||
<string name="share_link">Поделиться\nссылкой</string>
|
||||
<string name="delete_address">Удалить\nадрес</string>
|
||||
|
||||
@@ -549,6 +553,7 @@
|
||||
<string name="privacy_and_security">Конфиденциальность</string>
|
||||
<string name="your_privacy">Конфиденциальность</string>
|
||||
<string name="auto_accept_images">Автоприем изображений</string>
|
||||
<string name="transfer_images_faster">Передавать изображения быстрее (BETA)</string>
|
||||
<string name="send_link_previews">Отправлять картинки ссылок</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
@@ -728,6 +733,14 @@
|
||||
<string name="snd_group_event_user_left">вы покинули группу</string>
|
||||
<string name="snd_group_event_group_profile_updated">профиль группы обновлен</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">поменял(а) адрес для вас</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">смена адреса…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">вы поменяли адрес для %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">смена адреса для %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">вы поменяли адрес</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">смена адреса…</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_member">член группы</string>
|
||||
<string name="group_member_role_admin">админ</string>
|
||||
@@ -774,9 +787,7 @@
|
||||
<string name="button_create_group_link">Создать ссылку</string>
|
||||
<string name="delete_link_question">Удалить ссылку?</string>
|
||||
<string name="delete_link">Удалить ссылку</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе.</string>
|
||||
<string name="if_you_later_delete_link_you_wont_lose_members">Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.</string>
|
||||
<string name="if_you_delete_group_link_you_wont_lose_members">Вы можете удалить ссылку, сохранив членов группы, которые через нее соединились.</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.</string>
|
||||
<string name="all_group_members_will_remain_connected">Все члены группы, которые соединились через эту ссылку, останутся в группе.</string>
|
||||
<string name="error_creating_link_for_group">Ошибка при создании ссылки группы</string>
|
||||
<string name="error_deleting_link_for_group">Ошибка при удалении ссылки группы</string>
|
||||
@@ -795,6 +806,7 @@
|
||||
<string name="role_in_group">Роль</string>
|
||||
<string name="change_role">Поменять роль</string>
|
||||
<string name="change_verb">Поменять</string>
|
||||
<string name="switch_verb">Переключить</string>
|
||||
<string name="change_member_role_question">Поменять роль в группе?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на \"%s\". Все в группе получат сообщение.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на \"%s\". Будет отправлено новое приглашение.</string>
|
||||
@@ -810,6 +822,7 @@
|
||||
<string name="receiving_via">Получение через</string>
|
||||
<string name="sending_via">Отправка через</string>
|
||||
<string name="network_status">Состояние сети</string>
|
||||
<string name="switch_receiving_address">Переключить адрес получения (BETA)</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Создать скрытую группу</string>
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
<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_group_link">via group link</string>
|
||||
<string name="description_via_group_link_incognito">incognito via group link</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>
|
||||
@@ -65,6 +67,7 @@
|
||||
<string name="error_deleting_group">Error deleting group</string>
|
||||
<string name="error_deleting_contact_request">Error deleting contact request</string>
|
||||
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
|
||||
<string name="error_changing_address">Error changing address</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||
@@ -213,6 +216,8 @@
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
<string name="icon_descr_server_status_error">Error</string>
|
||||
<string name="icon_descr_server_status_pending">Pending</string>
|
||||
<string name="switch_receiving_address_question">Switch receiving address?</string>
|
||||
<string name="switch_receiving_address_desc">This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Send Message</string>
|
||||
@@ -318,6 +323,7 @@
|
||||
<string name="invalid_contact_link">Invalid link!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">This link is not a valid connection link!</string>
|
||||
<string name="connection_request_sent">Connection request sent!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">You will be connected to group when the group host\'s device is online, please wait or check later!</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">Your contact can scan QR code from the app.</string>
|
||||
@@ -393,9 +399,7 @@
|
||||
<string name="create_address">Create address</string>
|
||||
<string name="delete_address__question">Delete address?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">All your contacts will remain connected.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you.</string>
|
||||
<string name="if_you_later_delete_address_you_wont_lose_contacts">If you later delete it - you won\'t lose your contacts made via the address.</string>
|
||||
<string name="if_you_delete_address_you_wont_lose_contacts">If you delete it - you won\'t lose your contacts made via this address.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you. You won\'t lose your contacts if you later delete it.</string>
|
||||
<string name="share_link">Share link</string>
|
||||
<string name="delete_address">Delete address</string>
|
||||
|
||||
@@ -549,6 +553,7 @@
|
||||
<string name="privacy_and_security">Privacy & security</string>
|
||||
<string name="your_privacy">Your privacy</string>
|
||||
<string name="auto_accept_images">Auto-accept images</string>
|
||||
<string name="transfer_images_faster">Transfer images faster (BETA)</string>
|
||||
<string name="send_link_previews">Send link previews</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
@@ -728,6 +733,14 @@
|
||||
<string name="snd_group_event_user_left">you left</string>
|
||||
<string name="snd_group_event_group_profile_updated">group profile updated</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">changed address for you</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">you changed address for %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">changing address for %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">you changed address</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_member">member</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
@@ -774,9 +787,7 @@
|
||||
<string name="button_create_group_link">Create link</string>
|
||||
<string name="delete_link_question">Delete link?</string>
|
||||
<string name="delete_link">Delete link</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">You can share a link or a QR code - anybody will be able to join the group.</string>
|
||||
<string name="if_you_later_delete_link_you_wont_lose_members">If you later delete it - you won\'t lose members of the group connected via the link.</string>
|
||||
<string name="if_you_delete_group_link_you_wont_lose_members">If you delete it - you won\'t lose members of the group connected via this link.</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">You can share a link or a QR code - anybody will be able to join the group. You won\'t lose members of the group if you later delete it.</string>
|
||||
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
|
||||
<string name="error_creating_link_for_group">Error creating group link</string>
|
||||
<string name="error_deleting_link_for_group">Error deleting group link</string>
|
||||
@@ -795,6 +806,7 @@
|
||||
<string name="role_in_group">Role</string>
|
||||
<string name="change_role">Change role</string>
|
||||
<string name="change_verb">Change</string>
|
||||
<string name="switch_verb">Switch</string>
|
||||
<string name="change_member_role_question">Change group role?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string>
|
||||
@@ -810,6 +822,7 @@
|
||||
<string name="receiving_via">Receiving via</string>
|
||||
<string name="sending_via">Sending via</string>
|
||||
<string name="network_status">Network status</string>
|
||||
<string name="switch_receiving_address">Switch receiving address (BETA)</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Create secret group</string>
|
||||
|
||||
@@ -357,6 +357,14 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSwitchContact(contactId: Int64) async throws {
|
||||
try await sendCommandOkResp(.apiSwitchContact(contactId: contactId))
|
||||
}
|
||||
|
||||
func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws {
|
||||
try await sendCommandOkResp(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId))
|
||||
}
|
||||
|
||||
func apiAddContact() async -> String? {
|
||||
let r = await chatSendCmd(.addContact, bgTask: false)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
@@ -541,13 +549,14 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
||||
}
|
||||
|
||||
func receiveFile(fileId: Int64) async {
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId) {
|
||||
let inline = privacyTransferImagesInlineGroupDefault.get()
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, inline: inline) {
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(chatItem) }
|
||||
}
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId))
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
let am = AlertManager.shared
|
||||
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
|
||||
if case .rcvFileAcceptedSndCancelled = r {
|
||||
@@ -707,7 +716,7 @@ enum JoinGroupResult {
|
||||
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
|
||||
let r = await chatSendCmd(.apiJoinGroup(groupId: groupId))
|
||||
switch r {
|
||||
case let .userAcceptedGroupSent(groupInfo): return .joined(groupInfo: groupInfo)
|
||||
case let .userAcceptedGroupSent(groupInfo, _): return .joined(groupInfo: groupInfo)
|
||||
case .chatCmdError(.errorAgent(.SMP(.AUTH))): return .invitationRemoved
|
||||
case .chatCmdError(.errorStore(.groupNotFound)): return .groupNotFound
|
||||
default: throw r
|
||||
@@ -911,6 +920,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
if m.hasChat(toContact.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
}
|
||||
case let .contactsMerged(intoContact, mergedContact):
|
||||
if m.hasChat(mergedContact.id) {
|
||||
if m.chatId == mergedContact.id {
|
||||
m.chatId = intoContact.id
|
||||
}
|
||||
m.removeChat(mergedContact.id)
|
||||
}
|
||||
case let .contactsSubscribed(_, contactRefs):
|
||||
updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
@@ -975,6 +991,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
chatItems: []
|
||||
))
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
case let .userAcceptedGroupSent(groupInfo, hostContact):
|
||||
m.updateGroup(groupInfo)
|
||||
if let hostContact = hostContact {
|
||||
m.dismissConnReqView(hostContact.activeConn.id)
|
||||
m.removeChat(hostContact.activeConn.id)
|
||||
}
|
||||
case let .joinedGroupMemberConnecting(groupInfo, _, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .deletedMemberUser(groupInfo, _): // TODO update user member
|
||||
|
||||
@@ -30,7 +30,14 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
|
||||
@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]?) -> some View {
|
||||
if let servers = servers,
|
||||
servers.count > 0 {
|
||||
infoRow(title, serverHost(servers[0]))
|
||||
HStack {
|
||||
Text(title).frame(width: 120, alignment: .leading)
|
||||
Button(serverHost(servers[0])) {
|
||||
UIPasteboard.general.string = servers.joined(separator: ";")
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +54,7 @@ struct ChatInfoView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
var contact: Contact
|
||||
var connectionStats: ConnectionStats?
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
var customUserProfile: Profile?
|
||||
@State var localAlias: String
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
@@ -58,12 +65,16 @@ struct ChatInfoView: View {
|
||||
case deleteContactAlert
|
||||
case clearChatAlert
|
||||
case networkStatusAlert
|
||||
case switchAddressAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .deleteContactAlert: return "deleteContactAlert"
|
||||
case .clearChatAlert: return "clearChatAlert"
|
||||
case .networkStatusAlert: return "networkStatusAlert"
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,12 +99,17 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
networkStatusRow()
|
||||
.onTapGesture {
|
||||
alert = .networkStatusAlert
|
||||
}
|
||||
Section("Servers") {
|
||||
networkStatusRow()
|
||||
.onTapGesture {
|
||||
alert = .networkStatusAlert
|
||||
}
|
||||
if developerTools {
|
||||
Button("Change receiving address (BETA)") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
}
|
||||
if let connStats = connectionStats {
|
||||
smpServers("Receiving via", connStats.rcvServers)
|
||||
smpServers("Sending via", connStats.sndServers)
|
||||
}
|
||||
@@ -119,6 +135,8 @@ struct ChatInfoView: View {
|
||||
case .deleteContactAlert: return deleteContactAlert()
|
||||
case .clearChatAlert: return clearChatAlert()
|
||||
case .networkStatusAlert: return networkStatusAlert()
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +244,11 @@ struct ChatInfoView: View {
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error deleting contact")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -254,10 +276,38 @@ struct ChatInfoView: View {
|
||||
message: Text(chat.serverInfo.networkStatus.statusExplanation)
|
||||
)
|
||||
}
|
||||
|
||||
private func switchContactAddress() {
|
||||
Task {
|
||||
do {
|
||||
try await apiSwitchContact(contactId: contact.apiId)
|
||||
} catch let error {
|
||||
logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing address")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
||||
Alert(
|
||||
title: Text("Change receiving address?"),
|
||||
message: Text("This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)."),
|
||||
primaryButton: .destructive(Text("Change"), action: switchAddress),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, localAlias: "")
|
||||
ChatInfoView(
|
||||
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
|
||||
contact: Contact.sampleData,
|
||||
connectionStats: Binding.constant(nil),
|
||||
localAlias: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// CIGroupEventView.swift
|
||||
// CIEventView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by JRoberts on 20.07.2022.
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIGroupEventView: View {
|
||||
struct CIEventView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -43,8 +43,8 @@ struct CIGroupEventView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct CIGroupEventView_Previews: PreviewProvider {
|
||||
struct CIEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIGroupEventView(chatItem: ChatItem.getGroupEventSample())
|
||||
CIEventView(chatItem: ChatItem.getGroupEventSample())
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,10 @@ struct ChatItemView: View {
|
||||
case .rcvIntegrityError: IntegrityErrorItemView(chatItem: chatItem, showMember: showMember)
|
||||
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case .rcvGroupEvent: groupEventItemView()
|
||||
case .sndGroupEvent: groupEventItemView()
|
||||
case .rcvGroupEvent: eventItemView()
|
||||
case .sndGroupEvent: eventItemView()
|
||||
case .rcvConnEvent: eventItemView()
|
||||
case .sndConnEvent: eventItemView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +54,8 @@ struct ChatItemView: View {
|
||||
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito)
|
||||
}
|
||||
|
||||
private func groupEventItemView() -> some View {
|
||||
CIGroupEventView(chatItem: chatItem)
|
||||
private func eventItemView() -> some View {
|
||||
CIEventView(chatItem: chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ struct ChatView: View {
|
||||
connectionStats = nil
|
||||
customUserProfile = nil
|
||||
}) {
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Button {
|
||||
@@ -393,7 +393,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedMember, onDismiss: { memberConnectionStats = nil }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats)
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: $memberConnectionStats)
|
||||
}
|
||||
} else {
|
||||
Rectangle().fill(.clear)
|
||||
|
||||
@@ -72,7 +72,7 @@ struct GroupChatInfoView: View {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
.sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats)
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: $connectionStats)
|
||||
}
|
||||
.sheet(isPresented: $showGroupProfile) {
|
||||
GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile)
|
||||
|
||||
@@ -14,7 +14,7 @@ struct GroupMemberInfoView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var groupInfo: GroupInfo
|
||||
@State var member: GroupMember
|
||||
var connectionStats: ConnectionStats?
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
@State private var newRole: GroupMemberRole = .member
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@@ -22,12 +22,14 @@ struct GroupMemberInfoView: View {
|
||||
enum GroupMemberInfoViewAlert: Identifiable {
|
||||
case removeMemberAlert
|
||||
case changeMemberRoleAlert(role: GroupMemberRole)
|
||||
case switchAddressAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .removeMemberAlert: return "removeMemberAlert"
|
||||
case let .changeMemberRoleAlert(role): return "changeMemberRoleAlert \(role.rawValue)"
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -77,9 +79,14 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
if developerTools {
|
||||
Button("Change receiving address (BETA)") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
}
|
||||
if let connStats = connectionStats {
|
||||
smpServers("Receiving via", connStats.rcvServers)
|
||||
smpServers("Sending via", connStats.sndServers)
|
||||
}
|
||||
@@ -105,6 +112,7 @@ struct GroupMemberInfoView: View {
|
||||
switch(alertItem) {
|
||||
case .removeMemberAlert: return removeMemberAlert()
|
||||
case .changeMemberRoleAlert: return changeMemberRoleAlert()
|
||||
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
@@ -210,10 +218,28 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func switchMemberAddress() {
|
||||
Task {
|
||||
do {
|
||||
try await apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
} catch let error {
|
||||
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing address")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupMemberInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData)
|
||||
GroupMemberInfoView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
member: GroupMember.sampleData,
|
||||
connectionStats: Binding.constant(nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ struct ChatListNavLink: View {
|
||||
ChatPreviewView(chat: chat)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
joinGroupButton(groupInfo.hostConnCustomUserProfileId)
|
||||
joinGroupButton()
|
||||
if groupInfo.canDelete {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
}
|
||||
@@ -137,7 +137,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View {
|
||||
private func joinGroupButton() -> some View {
|
||||
Button {
|
||||
joinGroup(chat.chatInfo.apiId)
|
||||
} label: {
|
||||
|
||||
@@ -46,17 +46,19 @@ struct ContactConnectionInfo: View {
|
||||
.onTapGesture { aliasTextFieldFocused = false }
|
||||
|
||||
Section {
|
||||
HStack(spacing: 20) {
|
||||
Image(systemName: "pencil")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 6)
|
||||
.onTapGesture { aliasTextFieldFocused = true }
|
||||
TextField("Set contact name…", text: $localAlias)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled(true)
|
||||
.focused($aliasTextFieldFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit(setConnectionAlias)
|
||||
if contactConnection.groupLinkId == nil {
|
||||
HStack(spacing: 20) {
|
||||
Image(systemName: "pencil")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 6)
|
||||
.onTapGesture { aliasTextFieldFocused = true }
|
||||
TextField("Set contact name…", text: $localAlias)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled(true)
|
||||
.focused($aliasTextFieldFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit(setConnectionAlias)
|
||||
}
|
||||
}
|
||||
|
||||
if contactConnection.initiated,
|
||||
@@ -123,7 +125,10 @@ struct ContactConnectionInfo: View {
|
||||
|
||||
private func contactConnectionText(_ contactConnection: PendingContactConnection) -> LocalizedStringKey {
|
||||
contactConnection.viaContactUri
|
||||
? "You will be connected when your connection request is accepted, please wait or check later!"
|
||||
? (contactConnection.groupLinkId != nil
|
||||
? "You will be connected to group when the group host's device is online, please wait or check later!"
|
||||
: "You will be connected when your connection request is accepted, please wait or check later!"
|
||||
)
|
||||
: "You will be connected when your contact's device is online, please wait or check later!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,39 @@ func connectViaLink(_ connectionLink: String, _ dismiss: DismissAction? = nil) {
|
||||
}
|
||||
}
|
||||
|
||||
struct CRData: Decodable {
|
||||
var type: String
|
||||
var groupLinkId: String?
|
||||
}
|
||||
|
||||
func parseLinkQueryData(_ connectionLink: String) -> CRData? {
|
||||
if let hashIndex = connectionLink.firstIndex(of: "#"),
|
||||
let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
|
||||
let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
|
||||
let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
|
||||
let d = data.data(using: .utf8),
|
||||
let crData = try? getJSONDecoder().decode(CRData.self, from: d) {
|
||||
return crData
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkCRDataGroup(_ crData: CRData) -> Bool {
|
||||
return crData.type == "group" && crData.groupLinkId != nil
|
||||
}
|
||||
|
||||
func groupLinkAlert(_ connectionLink: String) -> Alert {
|
||||
return Alert(
|
||||
title: Text("Connect via group link?"),
|
||||
message: Text("You will join a group this link refers to and connect to its group members."),
|
||||
primaryButton: .default(Text("Connect")) {
|
||||
connectViaLink(connectionLink)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
func connectionReqSentAlert(_ type: ConnReqType) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Connection request sent!",
|
||||
|
||||
@@ -81,7 +81,14 @@ struct PasteToConnectView: View {
|
||||
}
|
||||
|
||||
private func connect() {
|
||||
connectViaLink(connectionLink.trimmingCharacters(in: .whitespaces), dismiss)
|
||||
let link = connectionLink.trimmingCharacters(in: .whitespaces)
|
||||
if let crData = parseLinkQueryData(link),
|
||||
checkCRDataGroup(crData) {
|
||||
dismiss()
|
||||
AlertManager.shared.showAlert(groupLinkAlert(link))
|
||||
} else {
|
||||
connectViaLink(link, dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,13 @@ struct ScanToConnectView: View {
|
||||
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
Task { connectViaLink(r.string, dismiss) }
|
||||
if let crData = parseLinkQueryData(r.string),
|
||||
checkCRDataGroup(crData) {
|
||||
dismiss()
|
||||
AlertManager.shared.showAlert(groupLinkAlert(r.string))
|
||||
} else {
|
||||
Task { connectViaLink(r.string, dismiss) }
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
||||
dismiss()
|
||||
|
||||
@@ -12,6 +12,8 @@ import SimpleXChat
|
||||
struct PrivacySettings: View {
|
||||
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
||||
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE, store: groupDefaults) private var transferImagesInline = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -22,7 +24,14 @@ struct PrivacySettings: View {
|
||||
Section("Chats") {
|
||||
settingsRow("photo") {
|
||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||
.onChange(of: autoAcceptImages) { privacyAcceptImagesGroupDefault.set($0) }
|
||||
.onChange(of: autoAcceptImages) {
|
||||
privacyAcceptImagesGroupDefault.set($0)
|
||||
}
|
||||
}
|
||||
if developerTools {
|
||||
settingsRow("photo.on.rectangle") {
|
||||
Toggle("Transfer images faster (BETA)", isOn: $transferImagesInline)
|
||||
}
|
||||
}
|
||||
settingsRow("network") {
|
||||
Toggle("Send link previews", isOn: $useLinkPreviews)
|
||||
|
||||
@@ -179,17 +179,17 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="1 day" xml:space="preserve">
|
||||
<source>1 day</source>
|
||||
<target>1 Tag</target>
|
||||
<target>täglich</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 month" xml:space="preserve">
|
||||
<source>1 month</source>
|
||||
<target>1 Monat</target>
|
||||
<target>monatlich</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 week" xml:space="preserve">
|
||||
<source>1 week</source>
|
||||
<target>1 Woche</target>
|
||||
<target>wöchentlich</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="6" xml:space="preserve">
|
||||
@@ -253,6 +253,11 @@
|
||||
<target>Inkognito akzeptieren</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept requests" xml:space="preserve">
|
||||
<source>Accept requests</source>
|
||||
<target>Anfragen annehmen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced network settings" xml:space="preserve">
|
||||
<source>Advanced network settings</source>
|
||||
<target>Erweiterte Netzwerkeinstellungen</target>
|
||||
@@ -260,7 +265,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="All group members will remain connected." xml:space="preserve">
|
||||
<source>All group members will remain connected.</source>
|
||||
<target>***All group members will remain connected.</target>
|
||||
<target>Alle Gruppenmitglieder bleiben verbunden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
|
||||
@@ -318,6 +323,11 @@
|
||||
<target>Bilder automatisch akzeptieren</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Automatically" xml:space="preserve">
|
||||
<source>Automatically</source>
|
||||
<target>Automatisch</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Anruf ist bereits beendet!</target>
|
||||
@@ -368,6 +378,16 @@
|
||||
<target>Die Mitgliederrolle ändern?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
|
||||
<source>Change receiving address (BETA)</source>
|
||||
<target>***Change receiving address (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address?" xml:space="preserve">
|
||||
<source>Change receiving address?</source>
|
||||
<target>***Change receiving address?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change role" xml:space="preserve">
|
||||
<source>Change role</source>
|
||||
<target>Rolle ändern</target>
|
||||
@@ -478,6 +498,11 @@
|
||||
<target>Über den Kontakt-Link verbinden?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via group link?" xml:space="preserve">
|
||||
<source>Connect via group link?</source>
|
||||
<target>***Connect via group link?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via link" xml:space="preserve">
|
||||
<source>Connect via link</source>
|
||||
<target>Über einen Link verbinden</target>
|
||||
@@ -568,6 +593,11 @@
|
||||
<target>Kontaktname</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact requests" xml:space="preserve">
|
||||
<source>Contact requests</source>
|
||||
<target>Kontaktanfragen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Kopieren</target>
|
||||
@@ -585,7 +615,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Create link" xml:space="preserve">
|
||||
<source>Create link</source>
|
||||
<target>***Create link</target>
|
||||
<target>Link erzeugen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
@@ -803,12 +833,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link" xml:space="preserve">
|
||||
<source>Delete link</source>
|
||||
<target>***Delete link</target>
|
||||
<target>Link löschen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link?" xml:space="preserve">
|
||||
<source>Delete link?</source>
|
||||
<target>***Delete link?</target>
|
||||
<target>Link löschen?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete message?" xml:space="preserve">
|
||||
@@ -823,7 +853,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete messages after" xml:space="preserve">
|
||||
<source>Delete messages after</source>
|
||||
<target>Löschen der Nachrichten nach</target>
|
||||
<target>Löschen der Nachrichten</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete old database" xml:space="preserve">
|
||||
@@ -1006,6 +1036,11 @@
|
||||
<target>Fehler beim Hinzufügen von Mitgliedern</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>***Error changing address</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Fehler beim Ändern der Rolle</target>
|
||||
@@ -1028,7 +1063,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating group link" xml:space="preserve">
|
||||
<source>Error creating group link</source>
|
||||
<target>***Error creating group link</target>
|
||||
<target>Fehler beim Erzeugen des Gruppen-Links</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
@@ -1046,6 +1081,11 @@
|
||||
<target>Fehler beim Löschen der Verbindung</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting contact" xml:space="preserve">
|
||||
<source>Error deleting contact</source>
|
||||
<target>***Error deleting contact</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting database" xml:space="preserve">
|
||||
<source>Error deleting database</source>
|
||||
<target>Fehler beim Löschen der Datenbank</target>
|
||||
@@ -1233,7 +1273,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Group link" xml:space="preserve">
|
||||
<source>Group link</source>
|
||||
<target>***Group link</target>
|
||||
<target>Gruppen-Link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -2335,7 +2375,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve">
|
||||
<source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source>
|
||||
<target>Diese Aktion kann nicht rückgängig gemacht werden - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, werden gelöscht. Dies kann mehrere Minuten dauern.</target>
|
||||
<target>Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dies kann mehrere Minuten dauern.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve">
|
||||
@@ -2343,6 +2383,11 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
<target>Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve">
|
||||
<source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source>
|
||||
<target>***This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This group no longer exists." xml:space="preserve">
|
||||
<source>This group no longer exists.</source>
|
||||
<target>Diese Gruppe existiert nicht mehr.</target>
|
||||
@@ -2385,6 +2430,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
|
||||
<target>Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
|
||||
<source>Transfer images faster (BETA)</source>
|
||||
<target>***Transfer images faster (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve">
|
||||
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
|
||||
<target>Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@).</target>
|
||||
@@ -2449,7 +2499,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
</trans-unit>
|
||||
<trans-unit id="Unread" xml:space="preserve">
|
||||
<source>Unread</source>
|
||||
<target>***Unread</target>
|
||||
<target>Ungelesen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update" xml:space="preserve">
|
||||
@@ -2532,6 +2582,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Willkommen %@!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message" xml:space="preserve">
|
||||
<source>Welcome message</source>
|
||||
<target>Begrüßungsmeldung</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When available" xml:space="preserve">
|
||||
<source>When available</source>
|
||||
<target>Wenn verfügbar</target>
|
||||
@@ -2594,7 +2649,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve">
|
||||
<source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source>
|
||||
<target>***You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</target>
|
||||
<target>Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve">
|
||||
@@ -2662,6 +2717,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Sie haben eine Gruppeneinladung gesendet</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected to group when the group host's device is online, please wait or check later!</source>
|
||||
<target>***You will be connected to group when the group host's device is online, please wait or check later!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
@@ -2677,6 +2737,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
|
||||
<source>You will join a group this link refers to and connect to its group members.</source>
|
||||
<target>***You will join a group this link refers to and connect to its group members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve">
|
||||
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
|
||||
<target>Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</target>
|
||||
@@ -2866,16 +2931,31 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Anrufen…</target>
|
||||
<note>call status</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed address for you" xml:space="preserve">
|
||||
<source>changed address for you</source>
|
||||
<target>***changed address for you</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
|
||||
<source>changed role of %1$@ to %2$@</source>
|
||||
<target>***changed role of %1$@ to %2$@</target>
|
||||
<target>änderte die Rolle von %1$@ auf %2$@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed your role to %@" xml:space="preserve">
|
||||
<source>changed your role to %@</source>
|
||||
<target>***changed your role to %@</target>
|
||||
<target>änderte Ihre Rolle auf %@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address for %@..." xml:space="preserve">
|
||||
<source>changing address for %@...</source>
|
||||
<target>***changing address for %@...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address..." xml:space="preserve">
|
||||
<source>changing address...</source>
|
||||
<target>***changing address...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="colored" xml:space="preserve">
|
||||
<source>colored</source>
|
||||
<target>farbig</target>
|
||||
@@ -3021,6 +3101,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Inkognito über einen Kontaktadressen Link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via group link" xml:space="preserve">
|
||||
<source>incognito via group link</source>
|
||||
<target>***incognito via group 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>Inkognito über einen Einmal-Link</target>
|
||||
@@ -3028,7 +3113,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="indirect (%d)" xml:space="preserve">
|
||||
<source>indirect (%d)</source>
|
||||
<target>Indirekt (%d)</target>
|
||||
<target>indirekt (%d)</target>
|
||||
<note>connection level description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invitation to group %@" xml:space="preserve">
|
||||
@@ -3043,7 +3128,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited %@" xml:space="preserve">
|
||||
<source>invited %@</source>
|
||||
<target>Sie haben %@ eingeladen</target>
|
||||
<target>hat %@ eingeladen.</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited to connect" xml:space="preserve">
|
||||
@@ -3053,7 +3138,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited via your group link" xml:space="preserve">
|
||||
<source>invited via your group link</source>
|
||||
<target>***invited via your group link</target>
|
||||
<target>wurde über Ihren Gruppen-Link eingeladen</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="italic" xml:space="preserve">
|
||||
@@ -3078,7 +3163,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="member connected" xml:space="preserve">
|
||||
<source>connected</source>
|
||||
<target>verbunden</target>
|
||||
<target>beigetreten</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="message received" xml:space="preserve">
|
||||
@@ -3143,12 +3228,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="removed %@" xml:space="preserve">
|
||||
<source>removed %@</source>
|
||||
<target>Entfernt %@</target>
|
||||
<target>hat %@ aus der Gruppe entfernt</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="removed you" xml:space="preserve">
|
||||
<source>removed you</source>
|
||||
<target>hat Sie entfernt</target>
|
||||
<target>hat Sie aus der Gruppe entfernt</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="sec" xml:space="preserve">
|
||||
@@ -3196,6 +3281,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>über einen Kontaktadressen Link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via group link" xml:space="preserve">
|
||||
<source>via group link</source>
|
||||
<target>***via group link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via one-time link" xml:space="preserve">
|
||||
<source>via one-time link</source>
|
||||
<target>über einen Einmal-Link</target>
|
||||
@@ -3231,14 +3321,24 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Sie sind zur Gruppe eingeladen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address" xml:space="preserve">
|
||||
<source>you changed address</source>
|
||||
<target>***you changed address</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address for %@" xml:space="preserve">
|
||||
<source>you changed address for %@</source>
|
||||
<target>***you changed address for %@</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
|
||||
<source>you changed role for yourself to %@</source>
|
||||
<target>***you changed role for yourself to %@</target>
|
||||
<target>Sie haben Ihre eigene Rolle auf %@ geändert</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role of %@ to %@" xml:space="preserve">
|
||||
<source>you changed role of %1$@ to %2$@</source>
|
||||
<target>***you changed role of %1$@ to %2$@</target>
|
||||
<target>Sie haben die Rolle von %1$@ auf %2$@ geändert</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you left" xml:space="preserve">
|
||||
@@ -3248,7 +3348,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="you removed %@" xml:space="preserve">
|
||||
<source>you removed %@</source>
|
||||
<target>Sie haben %@ entfernt</target>
|
||||
<target>entfernt %@ aus der Gruppe</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you shared one-time link" xml:space="preserve">
|
||||
|
||||
@@ -253,6 +253,11 @@
|
||||
<target>Accept incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept requests" xml:space="preserve">
|
||||
<source>Accept requests</source>
|
||||
<target>Accept requests</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced network settings" xml:space="preserve">
|
||||
<source>Advanced network settings</source>
|
||||
<target>Advanced network settings</target>
|
||||
@@ -318,6 +323,11 @@
|
||||
<target>Auto-accept images</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Automatically" xml:space="preserve">
|
||||
<source>Automatically</source>
|
||||
<target>Automatically</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Call already ended!</target>
|
||||
@@ -368,6 +378,16 @@
|
||||
<target>Change member role?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
|
||||
<source>Change receiving address (BETA)</source>
|
||||
<target>Change receiving address (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address?" xml:space="preserve">
|
||||
<source>Change receiving address?</source>
|
||||
<target>Change receiving address?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change role" xml:space="preserve">
|
||||
<source>Change role</source>
|
||||
<target>Change role</target>
|
||||
@@ -478,6 +498,11 @@
|
||||
<target>Connect via contact link?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via group link?" xml:space="preserve">
|
||||
<source>Connect via group link?</source>
|
||||
<target>Connect via group link?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via link" xml:space="preserve">
|
||||
<source>Connect via link</source>
|
||||
<target>Connect via link</target>
|
||||
@@ -568,6 +593,11 @@
|
||||
<target>Contact name</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact requests" xml:space="preserve">
|
||||
<source>Contact requests</source>
|
||||
<target>Contact requests</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Copy</target>
|
||||
@@ -1006,6 +1036,11 @@
|
||||
<target>Error adding member(s)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>Error changing address</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Error changing role</target>
|
||||
@@ -1046,6 +1081,11 @@
|
||||
<target>Error deleting connection</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting contact" xml:space="preserve">
|
||||
<source>Error deleting contact</source>
|
||||
<target>Error deleting contact</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting database" xml:space="preserve">
|
||||
<source>Error deleting database</source>
|
||||
<target>Error deleting database</target>
|
||||
@@ -2343,6 +2383,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve">
|
||||
<source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source>
|
||||
<target>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This group no longer exists." xml:space="preserve">
|
||||
<source>This group no longer exists.</source>
|
||||
<target>This group no longer exists.</target>
|
||||
@@ -2385,6 +2430,11 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>To support instant push notifications the chat database has to be migrated.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
|
||||
<source>Transfer images faster (BETA)</source>
|
||||
<target>Transfer images faster (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve">
|
||||
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
|
||||
<target>Trying to connect to the server used to receive messages from this contact (error: %@).</target>
|
||||
@@ -2532,6 +2582,11 @@ 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="Welcome message" xml:space="preserve">
|
||||
<source>Welcome message</source>
|
||||
<target>Welcome message</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>
|
||||
@@ -2662,6 +2717,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You sent group invitation</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected to group when the group host's device is online, please wait or check later!</source>
|
||||
<target>You will be connected to group when the group host's device is online, please wait or check later!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>You will be connected when your connection request is accepted, please wait or check later!</target>
|
||||
@@ -2677,6 +2737,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You will be required to authenticate when you start or resume the app after 30 seconds in background.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
|
||||
<source>You will join a group this link refers to and connect to its group members.</source>
|
||||
<target>You will join a group this link refers to and connect to its group members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve">
|
||||
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
|
||||
<target>You will stop receiving messages from this group. Chat history will be preserved.</target>
|
||||
@@ -2866,6 +2931,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>calling…</target>
|
||||
<note>call status</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed address for you" xml:space="preserve">
|
||||
<source>changed address for you</source>
|
||||
<target>changed address for you</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
|
||||
<source>changed role of %1$@ to %2$@</source>
|
||||
<target>changed role of %1$@ to %2$@</target>
|
||||
@@ -2876,6 +2946,16 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>changed your role to %@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address for %@..." xml:space="preserve">
|
||||
<source>changing address for %@...</source>
|
||||
<target>changing address for %@...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address..." xml:space="preserve">
|
||||
<source>changing address...</source>
|
||||
<target>changing address...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="colored" xml:space="preserve">
|
||||
<source>colored</source>
|
||||
<target>colored</target>
|
||||
@@ -3021,6 +3101,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>incognito via contact address link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via group link" xml:space="preserve">
|
||||
<source>incognito via group link</source>
|
||||
<target>incognito via group 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>
|
||||
@@ -3196,6 +3281,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>via contact address link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via group link" xml:space="preserve">
|
||||
<source>via group link</source>
|
||||
<target>via group link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via one-time link" xml:space="preserve">
|
||||
<source>via one-time link</source>
|
||||
<target>via one-time link</target>
|
||||
@@ -3231,6 +3321,16 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>you are invited to group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address" xml:space="preserve">
|
||||
<source>you changed address</source>
|
||||
<target>you changed address</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address for %@" xml:space="preserve">
|
||||
<source>you changed address for %@</source>
|
||||
<target>you changed address for %@</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
|
||||
<source>you changed role for yourself to %@</source>
|
||||
<target>you changed role for yourself to %@</target>
|
||||
|
||||
@@ -253,6 +253,11 @@
|
||||
<target>Принять инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept requests" xml:space="preserve">
|
||||
<source>Accept requests</source>
|
||||
<target>Принимать запросы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced network settings" xml:space="preserve">
|
||||
<source>Advanced network settings</source>
|
||||
<target>Настройки сети</target>
|
||||
@@ -318,6 +323,11 @@
|
||||
<target>Автоприем изображений</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Automatically" xml:space="preserve">
|
||||
<source>Automatically</source>
|
||||
<target>Автоматически</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Звонок уже завершен!</target>
|
||||
@@ -368,6 +378,16 @@
|
||||
<target>Поменять роль члена группы?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
|
||||
<source>Change receiving address (BETA)</source>
|
||||
<target>Поменять адрес получения (БЕТА)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address?" xml:space="preserve">
|
||||
<source>Change receiving address?</source>
|
||||
<target>Поменять адрес получения?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change role" xml:space="preserve">
|
||||
<source>Change role</source>
|
||||
<target>Поменять роль</target>
|
||||
@@ -478,6 +498,11 @@
|
||||
<target>Соединиться через ссылку-контакт?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via group link?" xml:space="preserve">
|
||||
<source>Connect via group link?</source>
|
||||
<target>Соединиться через ссылку группы?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via link" xml:space="preserve">
|
||||
<source>Connect via link</source>
|
||||
<target>Соединиться через ссылку</target>
|
||||
@@ -568,6 +593,11 @@
|
||||
<target>Имена контактов</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact requests" xml:space="preserve">
|
||||
<source>Contact requests</source>
|
||||
<target>Запросы контактов</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Скопировать</target>
|
||||
@@ -1006,6 +1036,11 @@
|
||||
<target>Ошибка при добавлении членов группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>Ошибка при изменении адреса</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Ошибка при изменении роли</target>
|
||||
@@ -1046,6 +1081,11 @@
|
||||
<target>Ошибка при удалении соединения</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting contact" xml:space="preserve">
|
||||
<source>Error deleting contact</source>
|
||||
<target>Ошибка при удалении контакта</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting database" xml:space="preserve">
|
||||
<source>Error deleting database</source>
|
||||
<target>Ошибка при удалении данных чата</target>
|
||||
@@ -2343,6 +2383,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="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve">
|
||||
<source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source>
|
||||
<target>Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This group no longer exists." xml:space="preserve">
|
||||
<source>This group no longer exists.</source>
|
||||
<target>Эта группа больше не существует.</target>
|
||||
@@ -2385,6 +2430,11 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
|
||||
<source>Transfer images faster (BETA)</source>
|
||||
<target>Передавать изображения быстрее (БЕТА)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve">
|
||||
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
|
||||
<target>Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@).</target>
|
||||
@@ -2532,6 +2582,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="Welcome message" xml:space="preserve">
|
||||
<source>Welcome message</source>
|
||||
<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>
|
||||
@@ -2662,6 +2717,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="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected to group when the group host's device is online, please wait or check later!</source>
|
||||
<target>Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</target>
|
||||
@@ -2677,6 +2737,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
|
||||
<source>You will join a group this link refers to and connect to its group members.</source>
|
||||
<target>Вы вступите в группу, на которую ссылается эта ссылка, и соединитесь с её членами.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve">
|
||||
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
|
||||
<target>Вы перестанете получать сообщения от этой группы. История чата будет сохранена.</target>
|
||||
@@ -2866,6 +2931,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>входящий звонок…</target>
|
||||
<note>call status</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed address for you" xml:space="preserve">
|
||||
<source>changed address for you</source>
|
||||
<target>поменял(а) адрес для вас</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
|
||||
<source>changed role of %1$@ to %2$@</source>
|
||||
<target>поменял(а) роль члена %1$@ на: %2$@</target>
|
||||
@@ -2876,6 +2946,16 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>поменял(а) вашу роль на: %@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address for %@..." xml:space="preserve">
|
||||
<source>changing address for %@...</source>
|
||||
<target>смена адреса для %@...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address..." xml:space="preserve">
|
||||
<source>changing address...</source>
|
||||
<target>смена адреса...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="colored" xml:space="preserve">
|
||||
<source>colored</source>
|
||||
<target>цвет</target>
|
||||
@@ -3021,6 +3101,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>инкогнито через ссылку-контакт</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via group link" xml:space="preserve">
|
||||
<source>incognito via group 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>
|
||||
@@ -3196,6 +3281,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>через ссылку-контакт</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via group link" xml:space="preserve">
|
||||
<source>via group link</source>
|
||||
<target>через ссылку группы</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via one-time link" xml:space="preserve">
|
||||
<source>via one-time link</source>
|
||||
<target>через одноразовую ссылку</target>
|
||||
@@ -3231,6 +3321,16 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>вы приглашены в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address" xml:space="preserve">
|
||||
<source>you changed address</source>
|
||||
<target>вы поменяли адрес</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address for %@" xml:space="preserve">
|
||||
<source>you changed address for %@</source>
|
||||
<target>вы поменяли адрес для %@</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
|
||||
<source>you changed role for yourself to %@</source>
|
||||
<target>вы поменяли роль себе на: %@</target>
|
||||
|
||||
@@ -220,7 +220,8 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= maxImageSize,
|
||||
privacyAcceptImagesGroupDefault.get() {
|
||||
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem
|
||||
let inline = privacyTransferImagesInlineGroupDefault.get()
|
||||
cItem = apiReceiveFile(fileId: file.fileId, inline: inline)?.chatItem ?? cItem
|
||||
}
|
||||
}
|
||||
return cItem.isCall() ? nil : (aChatItem.chatId, createMessageReceivedNtf(cInfo, cItem))
|
||||
@@ -274,8 +275,8 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId))
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
|
||||
logger.error("receiveFile error: \(responseError(r))")
|
||||
return nil
|
||||
|
||||
@@ -89,11 +89,6 @@
|
||||
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
|
||||
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
|
||||
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
|
||||
5CCA7DED2901E32900C8FEBA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DE82901E32800C8FEBA /* libgmp.a */; };
|
||||
5CCA7DEE2901E32900C8FEBA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DE92901E32800C8FEBA /* libffi.a */; };
|
||||
5CCA7DEF2901E32900C8FEBA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DEA2901E32800C8FEBA /* libgmpxx.a */; };
|
||||
5CCA7DF02901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DEB2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a */; };
|
||||
5CCA7DF12901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DEC2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a */; };
|
||||
5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */; };
|
||||
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
|
||||
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
|
||||
@@ -125,7 +120,12 @@
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
|
||||
6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIGroupEventView.swift */; };
|
||||
6432855F290BEE2B00FBE5C8 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855A290BEE2B00FBE5C8 /* libffi.a */; };
|
||||
64328560290BEE2B00FBE5C8 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855B290BEE2B00FBE5C8 /* libgmp.a */; };
|
||||
64328561290BEE2B00FBE5C8 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855C290BEE2B00FBE5C8 /* libgmpxx.a */; };
|
||||
64328562290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */; };
|
||||
64328563290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */; };
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
|
||||
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
|
||||
@@ -290,11 +290,6 @@
|
||||
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
|
||||
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CCA7DE82901E32800C8FEBA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CCA7DE92901E32800C8FEBA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CCA7DEA2901E32800C8FEBA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CCA7DEB2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CCA7DEC2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a"; sourceTree = "<group>"; };
|
||||
5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptRequestsView.swift; sourceTree = "<group>"; };
|
||||
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
|
||||
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
|
||||
@@ -324,7 +319,12 @@
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIGroupEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupEventView.swift; sourceTree = "<group>"; };
|
||||
6432855A290BEE2B00FBE5C8 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
6432855B290BEE2B00FBE5C8 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
6432855C290BEE2B00FBE5C8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a"; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
|
||||
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; };
|
||||
@@ -375,12 +375,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CCA7DF02901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a in Frameworks */,
|
||||
5CCA7DEF2901E32900C8FEBA /* libgmpxx.a in Frameworks */,
|
||||
5CCA7DED2901E32900C8FEBA /* libgmp.a in Frameworks */,
|
||||
64328561290BEE2B00FBE5C8 /* libgmpxx.a in Frameworks */,
|
||||
64328562290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a in Frameworks */,
|
||||
6432855F290BEE2B00FBE5C8 /* libffi.a in Frameworks */,
|
||||
64328560290BEE2B00FBE5C8 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CCA7DF12901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a in Frameworks */,
|
||||
5CCA7DEE2901E32900C8FEBA /* libffi.a in Frameworks */,
|
||||
64328563290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -435,11 +435,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CCA7DE92901E32800C8FEBA /* libffi.a */,
|
||||
5CCA7DE82901E32800C8FEBA /* libgmp.a */,
|
||||
5CCA7DEA2901E32800C8FEBA /* libgmpxx.a */,
|
||||
5CCA7DEB2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a */,
|
||||
5CCA7DEC2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a */,
|
||||
6432855A290BEE2B00FBE5C8 /* libffi.a */,
|
||||
6432855B290BEE2B00FBE5C8 /* libgmp.a */,
|
||||
6432855C290BEE2B00FBE5C8 /* libgmpxx.a */,
|
||||
6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */,
|
||||
6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -647,7 +647,7 @@
|
||||
5C029EA72837DBB3004A9677 /* CICallItemView.swift */,
|
||||
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */,
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */,
|
||||
6440C9FF288857A10062C672 /* CIGroupEventView.swift */,
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */,
|
||||
);
|
||||
path = ChatItem;
|
||||
sourceTree = "<group>";
|
||||
@@ -966,7 +966,7 @@
|
||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
|
||||
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
|
||||
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */,
|
||||
6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */,
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
|
||||
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
||||
@@ -1211,7 +1211,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
CURRENT_PROJECT_VERSION = 88;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1253,7 +1253,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
CURRENT_PROJECT_VERSION = 88;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1332,7 +1332,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
CURRENT_PROJECT_VERSION = 88;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1362,7 +1362,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
CURRENT_PROJECT_VERSION = 88;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
||||
@@ -55,6 +55,8 @@ public enum ChatCommand {
|
||||
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
|
||||
case apiContactInfo(contactId: Int64)
|
||||
case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
|
||||
case apiSwitchContact(contactId: Int64)
|
||||
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
|
||||
case addContact
|
||||
case connect(connReq: String)
|
||||
case apiDeleteChat(type: ChatType, id: Int64)
|
||||
@@ -80,7 +82,7 @@ public enum ChatCommand {
|
||||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||
case receiveFile(fileId: Int64)
|
||||
case receiveFile(fileId: Int64, inline: Bool)
|
||||
case string(String)
|
||||
|
||||
public var cmdString: String {
|
||||
@@ -131,6 +133,8 @@ public enum ChatCommand {
|
||||
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
|
||||
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
|
||||
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
|
||||
case let .apiSwitchContact(contactId): return "/_switch @\(contactId)"
|
||||
case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
|
||||
case .addContact: return "/connect"
|
||||
case let .connect(connReq): return "/connect \(connReq)"
|
||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||
@@ -155,7 +159,7 @@ public enum ChatCommand {
|
||||
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
|
||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||
case let .receiveFile(fileId): return "/freceive \(fileId)"
|
||||
case let .receiveFile(fileId, inline): return "/freceive \(fileId) inline=\(onOff(inline))"
|
||||
case let .string(str): return str
|
||||
}
|
||||
}
|
||||
@@ -206,6 +210,8 @@ public enum ChatCommand {
|
||||
case .apiSetChatSettings: return "apiSetChatSettings"
|
||||
case .apiContactInfo: return "apiContactInfo"
|
||||
case .apiGroupMemberInfo: return "apiGroupMemberInfo"
|
||||
case .apiSwitchContact: return "apiSwitchContact"
|
||||
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
|
||||
case .addContact: return "addContact"
|
||||
case .connect: return "connect"
|
||||
case .apiDeleteChat: return "apiDeleteChat"
|
||||
@@ -241,7 +247,7 @@ public enum ChatCommand {
|
||||
}
|
||||
|
||||
func smpServersStr(smpServers: [String]) -> String {
|
||||
smpServers.isEmpty ? "default" : smpServers.joined(separator: ",")
|
||||
smpServers.isEmpty ? "default" : smpServers.joined(separator: ";")
|
||||
}
|
||||
|
||||
func chatItemTTLStr(seconds: Int64?) -> String {
|
||||
@@ -323,7 +329,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
// group events
|
||||
case groupCreated(groupInfo: GroupInfo)
|
||||
case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember)
|
||||
case userAcceptedGroupSent(groupInfo: GroupInfo)
|
||||
case userAcceptedGroupSent(groupInfo: GroupInfo, hostContact: Contact?)
|
||||
case userDeletedMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case leftMemberUser(groupInfo: GroupInfo)
|
||||
case groupMembers(group: Group)
|
||||
|
||||
@@ -14,6 +14,7 @@ let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
|
||||
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
|
||||
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
|
||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
|
||||
@@ -92,6 +93,8 @@ public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey:
|
||||
|
||||
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
|
||||
public let privacyTransferImagesInlineGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE)
|
||||
|
||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||
|
||||
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
||||
|
||||
@@ -433,6 +433,7 @@ public struct PendingContactConnection: Decodable, NamedChat {
|
||||
var pccAgentConnId: String
|
||||
var pccConnStatus: ConnStatus
|
||||
public var viaContactUri: Bool
|
||||
public var groupLinkId: String?
|
||||
public var customUserProfileId: Int64?
|
||||
public var connReqInv: String?
|
||||
public var localAlias: String
|
||||
@@ -477,10 +478,18 @@ public struct PendingContactConnection: Decodable, NamedChat {
|
||||
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")
|
||||
if groupLinkId != nil {
|
||||
if incognito {
|
||||
desc = NSLocalizedString("incognito via group link", comment: "chat list item description")
|
||||
} else {
|
||||
desc = NSLocalizedString("via group link", comment: "chat list item description")
|
||||
}
|
||||
} else {
|
||||
desc = NSLocalizedString("via contact address link", comment: "chat list item description")
|
||||
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 {
|
||||
@@ -711,6 +720,11 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupMemberRef: Decodable {
|
||||
var groupMemberId: Int64
|
||||
var profile: Profile
|
||||
}
|
||||
|
||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
||||
case member = "member"
|
||||
case admin = "admin"
|
||||
@@ -1096,6 +1110,8 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
||||
case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent)
|
||||
case sndGroupEvent(sndGroupEvent: SndGroupEvent)
|
||||
case rcvConnEvent(rcvConnEvent: RcvConnEvent)
|
||||
case sndConnEvent(sndConnEvent: SndConnEvent)
|
||||
|
||||
public var text: String {
|
||||
get {
|
||||
@@ -1111,6 +1127,8 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
|
||||
case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text
|
||||
case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text
|
||||
case let .sndConnEvent(sndConnEvent): return sndConnEvent.text
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1498,6 +1516,44 @@ public enum SndGroupEvent: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RcvConnEvent: Decodable {
|
||||
case switchQueue(phase: SwitchPhase)
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .switchQueue(phase):
|
||||
if case .completed = phase {
|
||||
return NSLocalizedString("changed address for you", comment: "chat item text")
|
||||
}
|
||||
return NSLocalizedString("changing address...", comment: "chat item text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndConnEvent: Decodable {
|
||||
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .switchQueue(phase, member):
|
||||
if let name = member?.profile.profileViewName {
|
||||
return phase == .completed
|
||||
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
|
||||
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@...", comment: "chat item text"), name)
|
||||
}
|
||||
return phase == .completed
|
||||
? NSLocalizedString("you changed address", comment: "chat item text")
|
||||
: NSLocalizedString("changing address...", comment: "chat item text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SwitchPhase: String, Decodable {
|
||||
case started
|
||||
case confirmed
|
||||
case completed
|
||||
}
|
||||
|
||||
public enum ChatItemTTL: Hashable, Identifiable, Comparable {
|
||||
case day
|
||||
case week
|
||||
|
||||
@@ -116,13 +116,13 @@
|
||||
"~strike~" = "\\~durchstreichen~";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"1 day" = "1 Tag";
|
||||
"1 day" = "täglich";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"1 month" = "1 Monat";
|
||||
"1 month" = "monatlich";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"1 week" = "1 Woche";
|
||||
"1 week" = "wöchentlich";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"6" = "6";
|
||||
@@ -161,6 +161,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Accept incognito" = "Inkognito akzeptieren";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Accept requests" = "Anfragen annehmen";
|
||||
|
||||
/* call status */
|
||||
"accepted call" = "Anruf angenommen";
|
||||
|
||||
@@ -171,7 +174,7 @@
|
||||
"Advanced network settings" = "Erweiterte Netzwerkeinstellungen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All group members will remain connected." = "***All group members will remain connected.";
|
||||
"All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.";
|
||||
@@ -209,6 +212,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Auto-accept images" = "Bilder automatisch akzeptieren";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Automatically" = "Automatisch";
|
||||
|
||||
/* integrity error chat item */
|
||||
"bad message hash" = "Ungültiger Nachrichten-Hash";
|
||||
|
||||
@@ -257,14 +263,29 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Change member role?" = "Die Mitgliederrolle ändern?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address (BETA)" = "***Change receiving address (BETA)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address?" = "***Change receiving address?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change role" = "Rolle ändern";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed role of %@ to %@" = "***changed role of %1$@ to %2$@";
|
||||
/* chat item text */
|
||||
"changed address for you" = "***changed address for you";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed your role to %@" = "***changed your role to %@";
|
||||
"changed role of %@ to %@" = "änderte die Rolle von %1$@ auf %2$@";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed your role to %@" = "änderte Ihre Rolle auf %@";
|
||||
|
||||
/* chat item text */
|
||||
"changing address for %@..." = "***changing address for %@...";
|
||||
|
||||
/* chat item text */
|
||||
"changing address..." = "***changing address...";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat archive" = "Datenbank Archiv";
|
||||
@@ -338,6 +359,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via contact link?" = "Über den Kontakt-Link verbinden?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via group link?" = "***Connect via group link?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via link" = "Über einen Link verbinden";
|
||||
|
||||
@@ -428,6 +452,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Contact name" = "Kontaktname";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact requests" = "Kontaktanfragen";
|
||||
|
||||
/* chat item action */
|
||||
"Copy" = "Kopieren";
|
||||
|
||||
@@ -438,7 +465,7 @@
|
||||
"Create address" = "Adresse erstellen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Create link" = "***Create link";
|
||||
"Create link" = "Link erzeugen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Create one-time invitation link" = "Erstellen Sie einen einmaligen Einladungslink";
|
||||
@@ -567,10 +594,10 @@
|
||||
"Delete invitation" = "Einladung löschen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete link" = "***Delete link";
|
||||
"Delete link" = "Link löschen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete link?" = "***Delete link?";
|
||||
"Delete link?" = "Link löschen?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete message?" = "Die Nachricht löschen?";
|
||||
@@ -579,7 +606,7 @@
|
||||
"Delete messages" = "Nachrichten löschen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete messages after" = "Löschen der Nachrichten nach";
|
||||
"Delete messages after" = "Löschen der Nachrichten";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete old database" = "Alte Datenbank löschen";
|
||||
@@ -713,6 +740,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing address" = "***Error changing address";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing role" = "Fehler beim Ändern der Rolle";
|
||||
|
||||
@@ -726,7 +756,7 @@
|
||||
"Error creating group" = "Fehler beim Erzeugen der Gruppe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error creating group link" = "***Error creating group link";
|
||||
"Error creating group link" = "Fehler beim Erzeugen des Gruppen-Links";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank";
|
||||
@@ -737,6 +767,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting connection" = "Fehler beim Löschen der Verbindung";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting contact" = "***Error deleting contact";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting database" = "Fehler beim Löschen der Datenbank";
|
||||
|
||||
@@ -852,7 +885,7 @@
|
||||
"Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Group link" = "***Group link";
|
||||
"Group link" = "Gruppen-Link";
|
||||
|
||||
/* notification */
|
||||
"Group message:" = "Grppennachricht:";
|
||||
@@ -938,6 +971,9 @@
|
||||
/* chat list item description */
|
||||
"incognito via contact address link" = "Inkognito über einen Kontaktadressen Link";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via group link" = "***incognito via group link";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via one-time link" = "Inkognito über einen Einmal-Link";
|
||||
|
||||
@@ -951,7 +987,7 @@
|
||||
"Incoming video call" = "Eingehender Videoanruf";
|
||||
|
||||
/* connection level description */
|
||||
"indirect (%d)" = "Indirekt (%d)";
|
||||
"indirect (%d)" = "indirekt (%d)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat)";
|
||||
@@ -981,13 +1017,13 @@
|
||||
"invited" = "eingeladen";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"invited %@" = "Sie haben %@ eingeladen";
|
||||
"invited %@" = "hat %@ eingeladen.";
|
||||
|
||||
/* chat list item title */
|
||||
"invited to connect" = "Für eine Verbindung eingeladen";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"invited via your group link" = "***invited via your group link";
|
||||
"invited via your group link" = "wurde über Ihren Gruppen-Link eingeladen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Für die sichere Speicherung des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen.";
|
||||
@@ -1074,7 +1110,7 @@
|
||||
"Member" = "Mitglied";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"member connected" = "verbunden";
|
||||
"member connected" = "beigetreten";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.";
|
||||
@@ -1341,10 +1377,10 @@
|
||||
"removed" = "entfernt";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"removed %@" = "Entfernt %@";
|
||||
"removed %@" = "hat %@ aus der Gruppe entfernt";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"removed you" = "hat Sie entfernt";
|
||||
"removed you" = "hat Sie aus der Gruppe entfernt";
|
||||
|
||||
/* chat item action */
|
||||
"Reply" = "Antwort";
|
||||
@@ -1608,7 +1644,7 @@
|
||||
"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, werden gelöscht. Dies kann mehrere Minuten dauern.";
|
||||
"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dies kann mehrere Minuten dauern.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.";
|
||||
@@ -1616,6 +1652,9 @@
|
||||
/* notification title */
|
||||
"this contact" = "Dieser Kontakt";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." = "***This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This group no longer exists." = "Diese Gruppe existiert nicht mehr.";
|
||||
|
||||
@@ -1640,6 +1679,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"To support instant push notifications the chat database has to be migrated." = "Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Transfer images faster (BETA)" = "***Transfer images faster (BETA)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@).";
|
||||
|
||||
@@ -1680,7 +1722,7 @@
|
||||
"Unmute" = "Stummschaltung aufheben";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unread" = "***Unread";
|
||||
"Unread" = "Ungelesen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update" = "Aktualisieren";
|
||||
@@ -1724,6 +1766,9 @@
|
||||
/* chat list item description */
|
||||
"via contact address link" = "über einen Kontaktadressen Link";
|
||||
|
||||
/* chat list item description */
|
||||
"via group link" = "***via group link";
|
||||
|
||||
/* chat list item description */
|
||||
"via one-time link" = "über einen Einmal-Link";
|
||||
|
||||
@@ -1757,6 +1802,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome %@!" = "Willkommen %@!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome message" = "Begrüßungsmeldung";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"When available" = "Wenn verfügbar";
|
||||
|
||||
@@ -1797,7 +1845,7 @@
|
||||
"You can set lock screen notification preview via settings." = "Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "***You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.";
|
||||
"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." = "Sie können Ihre Adresse als Link oder als QR-Code teilen – Jeder kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.";
|
||||
@@ -1808,11 +1856,17 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role for yourself to %@" = "***you changed role for yourself to %@";
|
||||
/* chat item text */
|
||||
"you changed address" = "***you changed address";
|
||||
|
||||
/* chat item text */
|
||||
"you changed address for %@" = "***you changed address for %@";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role of %@ to %@" = "***you changed role of %1$@ to %2$@";
|
||||
"you changed role for yourself to %@" = "Sie haben Ihre eigene Rolle auf %@ geändert";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role of %@ to %@" = "Sie haben die Rolle von %1$@ auf %2$@ geändert";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sie legen fest, über welche Server Sie Ihre Nachrichten **empfangen** und an Ihre Kontakte **senden**.";
|
||||
@@ -1845,7 +1899,7 @@
|
||||
"You rejected group invitation" = "Sie haben die Gruppeneinladung abgelehnt";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you removed %@" = "Sie haben %@ entfernt";
|
||||
"you removed %@" = "entfernt %@ aus der Gruppe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You sent group invitation" = "Sie haben eine Gruppeneinladung gesendet";
|
||||
@@ -1856,6 +1910,9 @@
|
||||
/* chat list item description */
|
||||
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "***You will be connected to group when the group host's device is online, please wait or check later!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected when your connection request is accepted, please wait or check later!" = "Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!";
|
||||
|
||||
@@ -1865,6 +1922,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will join a group this link refers to and connect to its group members." = "***You will join a group this link refers to and connect to its group members.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will stop receiving messages from this group. Chat history will be preserved." = "Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.";
|
||||
|
||||
|
||||
@@ -161,6 +161,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Accept incognito" = "Принять инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Accept requests" = "Принимать запросы";
|
||||
|
||||
/* call status */
|
||||
"accepted call" = " принятый звонок";
|
||||
|
||||
@@ -209,6 +212,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Auto-accept images" = "Автоприем изображений";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Automatically" = "Автоматически";
|
||||
|
||||
/* integrity error chat item */
|
||||
"bad message hash" = "ошибка хэш сообщения";
|
||||
|
||||
@@ -257,15 +263,30 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Change member role?" = "Поменять роль члена группы?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address (BETA)" = "Поменять адрес получения (БЕТА)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address?" = "Поменять адрес получения?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change role" = "Поменять роль";
|
||||
|
||||
/* chat item text */
|
||||
"changed address for you" = "поменял(а) адрес для вас";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed role of %@ to %@" = "поменял(а) роль члена %1$@ на: %2$@";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed your role to %@" = "поменял(а) вашу роль на: %@";
|
||||
|
||||
/* chat item text */
|
||||
"changing address for %@..." = "смена адреса для %@...";
|
||||
|
||||
/* chat item text */
|
||||
"changing address..." = "смена адреса...";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat archive" = "Архив чата";
|
||||
|
||||
@@ -338,6 +359,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via contact link?" = "Соединиться через ссылку-контакт?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via group link?" = "Соединиться через ссылку группы?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via link" = "Соединиться через ссылку";
|
||||
|
||||
@@ -428,6 +452,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Contact name" = "Имена контактов";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact requests" = "Запросы контактов";
|
||||
|
||||
/* chat item action */
|
||||
"Copy" = "Скопировать";
|
||||
|
||||
@@ -713,6 +740,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error adding member(s)" = "Ошибка при добавлении членов группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing address" = "Ошибка при изменении адреса";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing role" = "Ошибка при изменении роли";
|
||||
|
||||
@@ -737,6 +767,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting connection" = "Ошибка при удалении соединения";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting contact" = "Ошибка при удалении контакта";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting database" = "Ошибка при удалении данных чата";
|
||||
|
||||
@@ -938,6 +971,9 @@
|
||||
/* chat list item description */
|
||||
"incognito via contact address link" = "инкогнито через ссылку-контакт";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via group link" = "инкогнито через ссылку группы";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via one-time link" = "инкогнито через одноразовую ссылку";
|
||||
|
||||
@@ -1616,6 +1652,9 @@
|
||||
/* notification title */
|
||||
"this contact" = "этот контакт";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." = "Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This group no longer exists." = "Эта группа больше не существует.";
|
||||
|
||||
@@ -1640,6 +1679,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"To support instant push notifications the chat database has to be migrated." = "Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Transfer images faster (BETA)" = "Передавать изображения быстрее (БЕТА)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@).";
|
||||
|
||||
@@ -1724,6 +1766,9 @@
|
||||
/* chat list item description */
|
||||
"via contact address link" = "через ссылку-контакт";
|
||||
|
||||
/* chat list item description */
|
||||
"via group link" = "через ссылку группы";
|
||||
|
||||
/* chat list item description */
|
||||
"via one-time link" = "через одноразовую ссылку";
|
||||
|
||||
@@ -1757,6 +1802,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome %@!" = "Здравствуйте %@!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome message" = "Приветственное сообщение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"When available" = "Когда возможно";
|
||||
|
||||
@@ -1808,6 +1856,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You can use markdown to format messages:" = "Вы можете форматировать сообщения:";
|
||||
|
||||
/* chat item text */
|
||||
"you changed address" = "вы поменяли адрес";
|
||||
|
||||
/* chat item text */
|
||||
"you changed address for %@" = "вы поменяли адрес для %@";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role for yourself to %@" = "вы поменяли роль себе на: %@";
|
||||
|
||||
@@ -1856,6 +1910,9 @@
|
||||
/* chat list item description */
|
||||
"you shared one-time link incognito" = "вы создали ссылку инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected when your connection request is accepted, please wait or check later!" = "Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!";
|
||||
|
||||
@@ -1865,6 +1922,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will join a group this link refers to and connect to its group members." = "Вы вступите в группу, на которую ссылается эта ссылка, и соединитесь с её членами.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена.";
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ main = do
|
||||
welcome opts
|
||||
t <- withTerminal pure
|
||||
simplexChatTerminal terminalChatConfig opts t
|
||||
else simplexChatCore terminalChatConfig opts Nothing $ \_ cc -> do
|
||||
else simplexChatCore terminalChatConfig opts Nothing $ \user cc -> do
|
||||
r <- sendChatCmd cc chatCmd
|
||||
putStrLn $ serializeChatResponse r
|
||||
putStrLn $ serializeChatResponse (Just user) r
|
||||
threadDelay $ chatCmdDelay opts * 1000000
|
||||
|
||||
welcome :: ChatOpts -> IO ()
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 19aef52135b288dc92c4a2ec6d0be4b8283649ab
|
||||
tag: 029fc6e781d78249d0b3a17edff7416ef22afed1
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -199,7 +199,7 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T
|
||||
|
||||
### Messages to manage groups and add members
|
||||
|
||||
`x.grp.inv` message is sent to invite contact to the group via contact's direct connection and includes group member connection address. This message MUST only be sent by members with `admin` or `owner` role.
|
||||
`x.grp.inv` message is sent to invite contact to the group via contact's direct connection and includes group member connection address. This message MUST only be sent by members with `admin` or `owner` role. Optional `groupLinkId` is included when this message is sent to contacts connected via the user's group link. This identifier is a random byte sequence, with no global or even local uniqueness - it is only used for the user's invitations to a given group to provide confirmation to the contact that the group invitation is for the same group the contact was connecting to via the group link, so that the invitation can be automatically accepted by the contact - the contact compares it with the group link id contained in the group link uri's data field.
|
||||
|
||||
`x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user.
|
||||
|
||||
|
||||
@@ -108,6 +108,12 @@
|
||||
"invitedMember": {"ref": "memberIdRole"},
|
||||
"connRequest": {"ref": "connReqUri"},
|
||||
"groupProfile": {"ref": "profile"}
|
||||
},
|
||||
"optionalProperties": {
|
||||
"groupLinkId": {"ref": "base64url"},
|
||||
"metadata": {
|
||||
"comment": "used to identify invitation via group link"
|
||||
}
|
||||
}
|
||||
},
|
||||
"memberIdRole": {
|
||||
|
||||
@@ -554,6 +554,7 @@ export interface CRGroupMembers extends CR {
|
||||
export interface CRUserAcceptedGroupSent extends CR {
|
||||
type: "userAcceptedGroupSent"
|
||||
groupInfo: GroupInfo
|
||||
hostContact?: Contact // included when joining group via group link
|
||||
}
|
||||
|
||||
export interface CRUserDeletedMember extends CR {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."19aef52135b288dc92c4a2ec6d0be4b8283649ab" = "0xkhm9hplm55ms34ln7la8zm59ailsxziqbaj5y79z9lafysl15j";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."029fc6e781d78249d0b3a17edff7416ef22afed1" = "054bjknwr75n7rpi4q41x966mnf4903rvy402vwyp1y9njzbd20q";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
|
||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
|
||||
+2
-1
@@ -59,6 +59,8 @@ library
|
||||
Simplex.Chat.Migrations.M20221019_unread_chat
|
||||
Simplex.Chat.Migrations.M20221021_auto_accept__group_links
|
||||
Simplex.Chat.Migrations.M20221024_contact_used
|
||||
Simplex.Chat.Migrations.M20221025_chat_settings
|
||||
Simplex.Chat.Migrations.M20221029_group_link_id
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Options
|
||||
Simplex.Chat.ProfileGenerator
|
||||
@@ -70,7 +72,6 @@ library
|
||||
Simplex.Chat.Terminal.Notification
|
||||
Simplex.Chat.Terminal.Output
|
||||
Simplex.Chat.Types
|
||||
Simplex.Chat.Util
|
||||
Simplex.Chat.View
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
|
||||
+139
-68
@@ -55,7 +55,6 @@ import Simplex.Chat.ProfileGenerator (generateRandomProfile)
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent as Agent
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), AgentDatabase (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
@@ -300,7 +299,7 @@ processChatCommand = \case
|
||||
(agentConnId_, fileConnReq) <-
|
||||
if isJust fileInline
|
||||
then pure (Nothing, Nothing)
|
||||
else bimap Just Just <$> withAgent (\a -> createConnection a True SCMInvitation)
|
||||
else bimap Just Just <$> withAgent (\a -> createConnection a True SCMInvitation Nothing)
|
||||
let fileName = takeFileName file
|
||||
fileInvitation = FileInvitation {fileName, fileSize, fileConnReq, fileInline}
|
||||
withStore' $ \db -> do
|
||||
@@ -493,7 +492,7 @@ processChatCommand = \case
|
||||
-- functions below are called in separate transactions to prevent crashes on android
|
||||
-- (possibly, race condition on integrity check?)
|
||||
withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct
|
||||
withStore' $ \db -> deleteContact db userId ct
|
||||
withStore' $ \db -> deleteContact db user ct
|
||||
unsetActive $ ActiveC localDisplayName
|
||||
pure $ CRContactDeleted ct
|
||||
CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do
|
||||
@@ -537,6 +536,8 @@ processChatCommand = \case
|
||||
maxItemTs_ <- withStore' $ \db -> getGroupMaxItemTs db user gInfo
|
||||
forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo
|
||||
withStore' $ \db -> deleteGroupCIs db user gInfo
|
||||
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db user gInfo
|
||||
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
|
||||
gInfo' <- case maxItemTs_ of
|
||||
Just ts -> do
|
||||
withStore' $ \db -> updateGroupTs db user gInfo ts
|
||||
@@ -644,6 +645,9 @@ processChatCommand = \case
|
||||
withCurrentCall contactId $ \userId ct call ->
|
||||
updateCallItemStatus userId ct call receivedStatus Nothing $> Just call
|
||||
APIUpdateProfile profile -> withUser (`updateProfile` profile)
|
||||
APISetContactPrefs contactId prefs' -> withUser $ \user@User {userId} -> do
|
||||
ct <- withStore $ \db -> getContact db userId contactId
|
||||
updateContactPrefs user ct prefs'
|
||||
APISetContactAlias contactId localAlias -> withUser $ \User {userId} -> do
|
||||
ct' <- withStore $ \db -> do
|
||||
ct <- getContact db userId contactId
|
||||
@@ -716,6 +720,15 @@ processChatCommand = \case
|
||||
(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
|
||||
APISwitchContact contactId -> withUser $ \User {userId} -> do
|
||||
ct <- withStore $ \db -> getContact db userId contactId
|
||||
withAgent $ \a -> switchConnectionAsync a "" $ contactConnId ct
|
||||
pure CRCmdOk
|
||||
APISwitchGroupMember gId gMemberId -> withUser $ \user -> do
|
||||
m <- withStore $ \db -> getGroupMember db user gId gMemberId
|
||||
case memberConnId m of
|
||||
Just connId -> withAgent (\a -> switchConnectionAsync a "" connId) $> CRCmdOk
|
||||
_ -> throwChatError CEGroupMemberNotActive
|
||||
ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do
|
||||
chatId <- case cType of
|
||||
CTDirect -> withStore $ \db -> getContactIdByName db user name
|
||||
@@ -728,23 +741,29 @@ processChatCommand = \case
|
||||
GroupMemberInfo gName mName -> withUser $ \user -> do
|
||||
(gId, mId) <- withStore $ \db -> getGroupIdByName db user gName >>= \gId -> (gId,) <$> getGroupMemberIdByName db user gId mName
|
||||
processChatCommand $ APIGroupMemberInfo gId mId
|
||||
SwitchContact cName -> withUser $ \user -> do
|
||||
contactId <- withStore $ \db -> getContactIdByName db user cName
|
||||
processChatCommand $ APISwitchContact contactId
|
||||
SwitchGroupMember gName mName -> withUser $ \user -> do
|
||||
(gId, mId) <- withStore $ \db -> getGroupIdByName db user gName >>= \gId -> (gId,) <$> getGroupMemberIdByName db user gId mName
|
||||
processChatCommand $ APISwitchGroupMember gId mId
|
||||
ChatHelp section -> pure $ CRChatHelp section
|
||||
Welcome -> withUser $ pure . CRWelcome
|
||||
AddContact -> withUser $ \User {userId} -> withChatLock "addContact" . procCmd $ do
|
||||
-- [incognito] generate profile for connection
|
||||
incognito <- readTVarIO =<< asks incognitoMode
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation
|
||||
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation Nothing
|
||||
conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnNew incognitoProfile
|
||||
toView $ CRNewContactConnection conn
|
||||
pure $ CRInvitation cReq
|
||||
Connect (Just (ACR SCMInvitation cReq)) -> withUser $ \User {userId, profile} -> withChatLock "connect" . procCmd $ do
|
||||
Connect (Just (ACR SCMInvitation cReq)) -> withUser $ \user@User {userId} -> withChatLock "connect" . procCmd $ do
|
||||
-- [incognito] generate profile to send
|
||||
incognito <- readTVarIO =<< asks incognitoMode
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
let profileToSend = fromMaybe (fromLocalProfile profile) incognitoProfile
|
||||
let profileToSend = userProfileToSend user incognitoProfile Nothing
|
||||
connId <- withAgent $ \a -> joinConnection a True cReq . directMessage $ XInfo profileToSend
|
||||
conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnJoined incognitoProfile
|
||||
conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnJoined $ incognitoProfile $> profileToSend
|
||||
toView $ CRNewContactConnection conn
|
||||
pure CRSentConfirmation
|
||||
Connect (Just (ACR SCMContact cReq)) -> withUser $ \User {userId, profile} ->
|
||||
@@ -762,7 +781,7 @@ processChatCommand = \case
|
||||
processChatCommand $ APIClearChat (ChatRef CTDirect contactId)
|
||||
ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user)
|
||||
CreateMyAddress -> withUser $ \User {userId} -> withChatLock "createMyAddress" . procCmd $ do
|
||||
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact
|
||||
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact Nothing
|
||||
withStore $ \db -> createUserContactLink db userId connId cReq
|
||||
pure $ CRUserContactLinkCreated cReq
|
||||
DeleteMyAddress -> withUser $ \user -> withChatLock "deleteMyAddress" $ do
|
||||
@@ -833,7 +852,7 @@ processChatCommand = \case
|
||||
case contactMember contact members of
|
||||
Nothing -> do
|
||||
gVar <- asks idsDrg
|
||||
(agentConnId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation
|
||||
(agentConnId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation Nothing
|
||||
member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq
|
||||
sendInvitation member cReq
|
||||
pure $ CRSentGroupInvitation gInfo contact member
|
||||
@@ -852,7 +871,7 @@ processChatCommand = \case
|
||||
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
updateCIGroupInvitationStatus user
|
||||
pure $ CRUserAcceptedGroupSent g {membership = membership {memberStatus = GSMemAccepted}}
|
||||
pure $ CRUserAcceptedGroupSent g {membership = membership {memberStatus = GSMemAccepted}} Nothing
|
||||
where
|
||||
updateCIGroupInvitationStatus user@User {userId} = do
|
||||
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db user groupId
|
||||
@@ -905,7 +924,10 @@ processChatCommand = \case
|
||||
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) Nothing Nothing
|
||||
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci
|
||||
deleteMemberConnection user m
|
||||
withStore' $ \db -> updateGroupMemberStatus db userId m GSMemRemoved
|
||||
withStore' $ \db ->
|
||||
checkGroupMemberHasItems db user m >>= \case
|
||||
Just _ -> updateGroupMemberStatus db userId m GSMemRemoved
|
||||
Nothing -> deleteGroupMember db user m
|
||||
pure $ CRUserDeletedMember gInfo m {memberStatus = GSMemRemoved}
|
||||
APILeaveGroup groupId -> withUser $ \user@User {userId} -> do
|
||||
Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user groupId
|
||||
@@ -964,8 +986,10 @@ processChatCommand = \case
|
||||
when (userRole < GRAdmin) $ throwChatError CEGroupUserRole
|
||||
when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined gInfo)
|
||||
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
|
||||
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact
|
||||
withStore $ \db -> createGroupLink db user gInfo connId cReq
|
||||
groupLinkId <- GroupLinkId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16))
|
||||
let crClientData = encodeJSON $ CRDataGroup groupLinkId
|
||||
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact $ Just crClientData
|
||||
withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId
|
||||
pure $ CRGroupLinkCreated gInfo cReq
|
||||
APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do
|
||||
gInfo <- withStore $ \db -> getGroupInfo db user groupId
|
||||
@@ -1085,7 +1109,7 @@ processChatCommand = \case
|
||||
CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) (safeDecodeUtf8 msg)
|
||||
_ -> throwChatError $ CECommandError "not supported"
|
||||
connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse
|
||||
connectViaContact userId cReq profile = withChatLock "connectViaContact" $ do
|
||||
connectViaContact userId cReq@(CRContactUri ConnReqUriData {crClientData}) profile = withChatLock "connectViaContact" $ do
|
||||
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
|
||||
withStore' (\db -> getConnReqContactXContactId db userId cReqHash) >>= \case
|
||||
(Just contact, _) -> pure $ CRContactAlreadyExists contact
|
||||
@@ -1101,7 +1125,8 @@ processChatCommand = \case
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
let profileToSend = fromMaybe profile incognitoProfile
|
||||
connId <- withAgent $ \a -> joinConnection a True cReq $ directMessage (XContact profileToSend $ Just xContactId)
|
||||
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile
|
||||
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
|
||||
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId
|
||||
toView $ CRNewContactConnection conn
|
||||
pure $ CRSentInvitation incognitoProfile
|
||||
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
|
||||
@@ -1132,9 +1157,23 @@ processChatCommand = \case
|
||||
filter (\ct -> isReady ct && not (contactConnIncognito ct))
|
||||
<$> withStore' (`getUserContacts` user)
|
||||
withChatLock "updateProfile" . procCmd $ do
|
||||
forM_ contacts $ \ct ->
|
||||
void (sendDirectContactMessage ct $ XInfo p') `catchError` (toView . CRChatError)
|
||||
forM_ contacts $ \ct -> do
|
||||
let mergedProfile = userProfileToSend user' Nothing $ Just ct
|
||||
void (sendDirectContactMessage ct $ XInfo mergedProfile) `catchError` (toView . CRChatError)
|
||||
pure $ CRUserProfileUpdated (fromLocalProfile p) p'
|
||||
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
|
||||
updateContactPrefs user@User {userId} ct@Contact {contactId, activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
|
||||
| contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated ct ct $ contactUserPreferences user ct -- nothing changed actually
|
||||
| otherwise = do
|
||||
withStore' $ \db -> updateContactUserPreferences db userId contactId contactUserPrefs'
|
||||
-- [incognito] filter out contacts with whom user has incognito connections
|
||||
let ct' = (ct :: Contact) {userPreferences = contactUserPrefs'}
|
||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId
|
||||
let p' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct')
|
||||
withChatLock "updateProfile" . procCmd $ do
|
||||
void (sendDirectContactMessage ct' $ XInfo p') `catchError` (toView . CRChatError)
|
||||
pure $ CRContactPrefsUpdated ct ct' $ contactUserPreferences user ct'
|
||||
|
||||
isReady :: Contact -> Bool
|
||||
isReady ct =
|
||||
let s = connStatus $ activeConn (ct :: Contact)
|
||||
@@ -1175,7 +1214,7 @@ processChatCommand = \case
|
||||
sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> m ()
|
||||
sendGrpInvitation user ct@Contact {localDisplayName} GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
|
||||
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile
|
||||
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile Nothing
|
||||
(msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv
|
||||
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
|
||||
ci <- saveSndChatItem user (CDDirectSnd ct) msg content Nothing Nothing
|
||||
@@ -1298,7 +1337,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F
|
||||
| fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName
|
||||
| otherwise -> do
|
||||
-- accepting via a new connection
|
||||
(agentConnId, fileInvConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation
|
||||
(agentConnId, fileInvConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation Nothing
|
||||
ci <- withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnNew filePath
|
||||
pure (XFileAcptInv sharedMsgId (Just fileInvConnReq) fName, ci)
|
||||
receiveInline :: m Bool
|
||||
@@ -1350,25 +1389,26 @@ getRcvFilePath fileId fPath_ fn = case fPath_ of
|
||||
in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f)
|
||||
|
||||
acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
|
||||
acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do
|
||||
acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do
|
||||
let profileToSend = profileToSendOnAccept user incognitoProfile
|
||||
acId <- withAgent $ \a -> acceptContact a True invId . directMessage $ XInfo profileToSend
|
||||
withStore' $ \db -> createAcceptedContact db userId acId cName profileId p userContactLinkId xContactId incognitoProfile
|
||||
withStore' $ \db -> createAcceptedContact db user acId cName profileId cp userContactLinkId xContactId incognitoProfile
|
||||
|
||||
acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
|
||||
acceptContactRequestAsync user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do
|
||||
acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do
|
||||
let profileToSend = profileToSendOnAccept user incognitoProfile
|
||||
(cmdId, acId) <- agentAcceptContactAsync user True invId $ XInfo profileToSend
|
||||
withStore' $ \db -> do
|
||||
ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db userId acId cName profileId p userContactLinkId xContactId incognitoProfile
|
||||
ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cName profileId p userContactLinkId xContactId incognitoProfile
|
||||
setCommandConnId db user cmdId connId
|
||||
pure ct
|
||||
|
||||
profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Profile
|
||||
profileToSendOnAccept User {profile} = \case
|
||||
Just (NewIncognito p) -> p
|
||||
Just (ExistingIncognito lp) -> fromLocalProfile lp
|
||||
Nothing -> fromLocalProfile profile
|
||||
profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing
|
||||
where
|
||||
getIncognitoProfile = \case
|
||||
NewIncognito p -> p
|
||||
ExistingIncognito lp -> fromLocalProfile lp
|
||||
|
||||
deleteGroupLink' :: ChatMonad m => User -> GroupInfo -> m ()
|
||||
deleteGroupLink' user gInfo = do
|
||||
@@ -1536,6 +1576,8 @@ expireChatItems user ttl sync = do
|
||||
maxItemTs_ <- withStore' $ \db -> getGroupMaxItemTs db user gInfo
|
||||
forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo
|
||||
withStore' $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff
|
||||
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db user gInfo
|
||||
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
|
||||
withStore' $ \db -> do
|
||||
ciCount_ <- getGroupCICount db user gInfo
|
||||
case (maxItemTs_, ciCount_) of
|
||||
@@ -1564,7 +1606,7 @@ processAgentMessage (Just user) _ agentConnId END =
|
||||
showToast (c <> "> ") "connected to another client"
|
||||
unsetActive $ ActiveC c
|
||||
entity -> toView $ CRSubscriptionEnd entity
|
||||
processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentMessage =
|
||||
processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
||||
(withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= updateConnStatus) >>= \case
|
||||
RcvDirectMsgConnection conn contact_ ->
|
||||
processDirectMessage agentMessage conn contact_
|
||||
@@ -1602,7 +1644,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM
|
||||
CONF confId _ connInfo -> do
|
||||
-- [incognito] send saved profile
|
||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||
let profileToSend = fromLocalProfile $ fromMaybe profile incognitoProfile
|
||||
let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing
|
||||
saveConnInfo conn connInfo
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
allowAgentConnectionAsync user conn confId $ XInfo profileToSend
|
||||
@@ -1703,8 +1745,9 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM
|
||||
gVar <- asks idsDrg
|
||||
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation
|
||||
withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct GRMember groupConnIds
|
||||
probeMatchingContacts ct $ contactConnIncognito ct
|
||||
_ -> pure ()
|
||||
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) -> do
|
||||
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
|
||||
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
|
||||
notifyMemberConnected gInfo m
|
||||
let connectedIncognito = contactConnIncognito ct || memberIncognito membership
|
||||
@@ -1717,6 +1760,11 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM
|
||||
chatItem <- withStore $ \db -> updateDirectChatItemStatus db userId contactId (chatItemId' ci) CISSndSent
|
||||
toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
_ -> pure ()
|
||||
SWITCH qd phase cStats -> do
|
||||
toView . CRContactSwitch ct $ SwitchProgress qd phase cStats
|
||||
when (phase /= SPConfirmed) $ case qd of
|
||||
QDRcv -> createInternalChatItem (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing
|
||||
QDSnd -> createInternalChatItem (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
||||
@@ -1752,21 +1800,17 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM
|
||||
Nothing -> messageError "implementation error: invitee does not have contact"
|
||||
Just ct -> do
|
||||
withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
|
||||
sendGrpInvitation ct m
|
||||
groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
|
||||
sendGrpInvitation ct m groupLinkId
|
||||
toView $ CRSentGroupInvitation gInfo ct m
|
||||
where
|
||||
sendGrpInvitation :: Contact -> GroupMember -> m ()
|
||||
sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} = do
|
||||
sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m ()
|
||||
sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do
|
||||
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile
|
||||
groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile groupLinkId
|
||||
(_msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv
|
||||
createdAt <- liftIO getCurrentTime
|
||||
let content = CIRcvGroupEvent RGEInvitedViaGroupLink
|
||||
cd = CDGroupRcv gInfo m
|
||||
-- we could link chat item with sent group invitation message (_msg)
|
||||
ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content createdAt createdAt
|
||||
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing createdAt createdAt
|
||||
toView $ CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci
|
||||
createInternalChatItem (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
|
||||
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
|
||||
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
||||
CONF confId _ connInfo -> do
|
||||
@@ -1864,6 +1908,11 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM
|
||||
SENT msgId -> do
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
checkSndInlineFTComplete conn msgId
|
||||
SWITCH qd phase cStats -> do
|
||||
toView . CRGroupMemberSwitch gInfo m $ SwitchProgress qd phase cStats
|
||||
when (phase /= SPConfirmed) $ case qd of
|
||||
QDRcv -> createInternalChatItem (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing
|
||||
QDSnd -> createInternalChatItem (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
||||
@@ -2079,14 +2128,9 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM
|
||||
throwChatError $ CEFileRcvChunk err
|
||||
|
||||
memberConnectedChatItem :: GroupInfo -> GroupMember -> m ()
|
||||
memberConnectedChatItem gInfo m = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
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
|
||||
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing createdAt createdAt
|
||||
toView $ CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci
|
||||
memberConnectedChatItem gInfo m =
|
||||
-- ts should be broker ts but we don't have it for CON
|
||||
createInternalChatItem (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing
|
||||
|
||||
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
|
||||
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do
|
||||
@@ -2376,37 +2420,52 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM
|
||||
toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci
|
||||
|
||||
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||
processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {customUserProfileId}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole)} msg msgMeta = do
|
||||
processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {connId, customUserProfileId}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} msg msgMeta = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
when (fromRole < GRMember || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
|
||||
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
|
||||
-- [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
|
||||
showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group"
|
||||
(gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId
|
||||
groupLinkId' <- withStore' $ \db -> getConnectionGroupLinkId db user connId
|
||||
if sameGroupLinkId groupLinkId groupLinkId'
|
||||
then do
|
||||
connIds <- joinAgentConnectionAsync user True connRequest . directMessage $ XGrpAcpt memberId
|
||||
withStore' $ \db -> do
|
||||
createMemberConnectionAsync db user hostId connIds
|
||||
updateGroupMemberStatusById db userId hostId GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
toView $ CRUserAcceptedGroupSent gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct)
|
||||
else do
|
||||
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
|
||||
showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group"
|
||||
where
|
||||
sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool
|
||||
sameGroupLinkId (Just gli) (Just gli') = gli == gli'
|
||||
sameGroupLinkId _ _ = False
|
||||
|
||||
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
|
||||
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
|
||||
MsgOk -> pure ()
|
||||
MsgError e -> case e of
|
||||
MsgSkipped {} -> createIntegrityErrorItem e
|
||||
MsgSkipped {} -> createInternalChatItem cd (CIRcvIntegrityError e) (Just brokerTs)
|
||||
_ -> toView $ CRMsgIntegrityError e
|
||||
where
|
||||
createIntegrityErrorItem e = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
let content = CIRcvIntegrityError e
|
||||
ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content brokerTs createdAt
|
||||
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing brokerTs createdAt
|
||||
toView $ CRNewChatItem $ AChatItem (chatTypeI @c) SMDRcv (toChatInfo cd) ci
|
||||
|
||||
createInternalChatItem :: forall c d. (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> CIContent d -> Maybe UTCTime -> m ()
|
||||
createInternalChatItem cd content itemTs_ = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
let itemTs = fromMaybe createdAt itemTs_
|
||||
ciId <- withStore' $ \db -> createNewChatItemNoMsg db user cd content itemTs createdAt
|
||||
ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing itemTs createdAt
|
||||
toView $ CRNewChatItem $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
|
||||
|
||||
xInfo :: Contact -> Profile -> m ()
|
||||
xInfo c@Contact {profile = p} p' = unless (fromLocalProfile p == p') $ do
|
||||
c' <- withStore $ \db -> updateContactProfile db userId c p'
|
||||
toView $ CRContactUpdated c c'
|
||||
toView $ CRContactUpdated c c' $ contactUserPreferences user c'
|
||||
|
||||
xInfoProbe :: Contact -> Probe -> m ()
|
||||
xInfoProbe c2 probe =
|
||||
@@ -2963,6 +3022,12 @@ deleteAgentConnectionAsync' user connId (AgentConnId acId) = do
|
||||
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFDeleteConn
|
||||
withAgent $ \a -> deleteConnectionAsync a (aCorrId cmdId) acId
|
||||
|
||||
userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Profile
|
||||
userProfileToSend user@User {profile = p} incognitoProfile ct =
|
||||
let p' = fromMaybe (fromLocalProfile p) incognitoProfile
|
||||
userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile
|
||||
in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs}
|
||||
|
||||
getCreateActiveUser :: SQLiteStore -> IO User
|
||||
getCreateActiveUser st = do
|
||||
user <-
|
||||
@@ -2984,7 +3049,7 @@ getCreateActiveUser st = do
|
||||
loop = do
|
||||
displayName <- getContactName
|
||||
fullName <- T.pack <$> getWithPrompt "full name (optional)"
|
||||
withTransaction st (\db -> runExceptT $ createUser db Profile {displayName, fullName, image = Nothing} True) >>= \case
|
||||
withTransaction st (\db -> runExceptT $ createUser db Profile {displayName, fullName, image = Nothing, preferences = Nothing} True) >>= \case
|
||||
Left SEDuplicateName -> do
|
||||
putStrLn "chosen display name is already used by another profile on this device, choose another one"
|
||||
loop
|
||||
@@ -3117,6 +3182,7 @@ chatCommandP =
|
||||
"/_profile " *> (APIUpdateProfile <$> jsonP),
|
||||
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||
"/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP),
|
||||
"/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString),
|
||||
"/_ntf get" $> APIGetNtfToken,
|
||||
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
|
||||
@@ -3143,6 +3209,10 @@ chatCommandP =
|
||||
"/_info @" *> (APIContactInfo <$> A.decimal),
|
||||
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
|
||||
("/info @" <|> "/info " <|> "/i @" <|> "/i ") *> (ContactInfo <$> displayName),
|
||||
"/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_switch @" *> (APISwitchContact <$> A.decimal),
|
||||
"/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
|
||||
("/switch @" <|> "/switch ") *> (SwitchContact <$> displayName),
|
||||
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
|
||||
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
|
||||
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
|
||||
@@ -3163,6 +3233,7 @@ chatCommandP =
|
||||
("/members #" <|> "/members " <|> "/ms #" <|> "/ms ") *> (ListMembers <$> displayName),
|
||||
("/groups" <|> "/gs") $> ListGroups,
|
||||
"/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP),
|
||||
-- TODO group profile update via terminal should not reset image and preferences to Nothing (now it does)
|
||||
("/group_profile #" <|> "/gp #" <|> "/group_profile " <|> "/gp ") *> (UpdateGroupProfile <$> displayName <* A.space <*> groupProfile),
|
||||
"/_create link #" *> (APICreateGroupLink <$> A.decimal),
|
||||
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
|
||||
@@ -3229,13 +3300,13 @@ chatCommandP =
|
||||
pure (cName, fullName)
|
||||
userProfile = do
|
||||
(cName, fullName) <- userNames
|
||||
pure Profile {displayName = cName, fullName, image = Nothing}
|
||||
pure Profile {displayName = cName, fullName, image = Nothing, preferences = Nothing}
|
||||
jsonP :: J.FromJSON a => Parser a
|
||||
jsonP = J.eitherDecodeStrict' <$?> A.takeByteString
|
||||
groupProfile = do
|
||||
gName <- displayName
|
||||
fullName <- fullNameP gName
|
||||
pure GroupProfile {displayName = gName, fullName, image = Nothing}
|
||||
pure GroupProfile {displayName = gName, fullName, image = Nothing, groupPreferences = Nothing}
|
||||
fullNameP name = do
|
||||
n <- (A.space *> A.takeByteString) <|> pure ""
|
||||
pure $ if B.null n then name else safeDecodeUtf8 n
|
||||
|
||||
@@ -21,7 +21,7 @@ import qualified Database.SQLite3 as SQL
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Agent.Client (agentClientStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString)
|
||||
import Simplex.Messaging.Util (unlessM, whenM)
|
||||
import Simplex.Messaging.Util
|
||||
import System.FilePath
|
||||
import UnliftIO.Directory
|
||||
import UnliftIO.Exception (SomeException, bracket, catch)
|
||||
|
||||
@@ -15,16 +15,13 @@ import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Types (Contact, ContactId)
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Chat.Types (Contact, ContactId, decodeJSON, encodeJSON)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON)
|
||||
@@ -103,10 +100,10 @@ instance ToJSON CallState where
|
||||
toEncoding = J.genericToEncoding $ singleFieldJSON fstToLower
|
||||
|
||||
instance ToField CallState where
|
||||
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode
|
||||
toField = toField . encodeJSON
|
||||
|
||||
instance FromField CallState where
|
||||
fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8
|
||||
fromField = fromTextField_ decodeJSON
|
||||
|
||||
newtype CallId = CallId ByteString
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -89,7 +89,7 @@ defaultInlineFilesConfig =
|
||||
{ offerChunks = 15, -- max when chunks are offered / received with the option - limited to 255 on the encoding level
|
||||
sendChunks = 0, -- max per file when chunks will be sent inline without acceptance
|
||||
totalSendChunks = 30, -- max per conversation when chunks will be sent inline without acceptance
|
||||
receiveChunks = 5 -- max when chunks are accepted
|
||||
receiveChunks = 6 -- max when chunks are accepted
|
||||
}
|
||||
|
||||
data ActiveTo = ActiveNone | ActiveC ContactName | ActiveG GroupName
|
||||
@@ -166,6 +166,7 @@ data ChatCommand
|
||||
| APIGetCallInvitations
|
||||
| APICallStatus ContactId WebRTCCallStatus
|
||||
| APIUpdateProfile Profile
|
||||
| APISetContactPrefs Int64 Preferences
|
||||
| APISetContactAlias ContactId LocalAlias
|
||||
| APISetConnectionAlias Int64 LocalAlias
|
||||
| APIParseMarkdown Text
|
||||
@@ -193,9 +194,13 @@ data ChatCommand
|
||||
| APISetChatSettings ChatRef ChatSettings
|
||||
| APIContactInfo ContactId
|
||||
| APIGroupMemberInfo GroupId GroupMemberId
|
||||
| APISwitchContact ContactId
|
||||
| APISwitchGroupMember GroupId GroupMemberId
|
||||
| ShowMessages ChatName Bool
|
||||
| ContactInfo ContactName
|
||||
| GroupMemberInfo GroupName ContactName
|
||||
| SwitchContact ContactName
|
||||
| SwitchGroupMember GroupName ContactName
|
||||
| ChatHelp HelpSection
|
||||
| Welcome
|
||||
| AddContact
|
||||
@@ -261,6 +266,8 @@ data ChatResponse
|
||||
| CRNetworkConfig {networkConfig :: NetworkConfig}
|
||||
| CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile}
|
||||
| CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
|
||||
| CRContactSwitch {contact :: Contact, switchProgress :: SwitchProgress}
|
||||
| CRGroupMemberSwitch {groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress}
|
||||
| CRNewChatItem {chatItem :: AChatItem}
|
||||
| CRChatItemStatusUpdated {chatItem :: AChatItem}
|
||||
| CRChatItemUpdated {chatItem :: AChatItem}
|
||||
@@ -278,7 +285,7 @@ data ChatResponse
|
||||
| CRUserContactLink {contactLink :: UserContactLink}
|
||||
| CRUserContactLinkUpdated {contactLink :: UserContactLink}
|
||||
| CRContactRequestRejected {contactRequest :: UserContactRequest}
|
||||
| CRUserAcceptedGroupSent {groupInfo :: GroupInfo}
|
||||
| CRUserAcceptedGroupSent {groupInfo :: GroupInfo, hostContact :: Maybe Contact}
|
||||
| CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupsList {groups :: [GroupInfo]}
|
||||
| CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
@@ -289,7 +296,7 @@ data ChatResponse
|
||||
| CRInvitation {connReqInvitation :: ConnReqInvitation}
|
||||
| CRSentConfirmation
|
||||
| CRSentInvitation {customUserProfile :: Maybe Profile}
|
||||
| CRContactUpdated {fromContact :: Contact, toContact :: Contact}
|
||||
| CRContactUpdated {fromContact :: Contact, toContact :: Contact, preferences :: ContactUserPreferences}
|
||||
| CRContactsMerged {intoContact :: Contact, mergedContact :: Contact}
|
||||
| CRContactDeleted {contact :: Contact}
|
||||
| CRChatCleared {chatInfo :: AChatInfo}
|
||||
@@ -315,6 +322,7 @@ data ChatResponse
|
||||
| CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile}
|
||||
| CRContactAliasUpdated {toContact :: Contact}
|
||||
| CRConnectionAliasUpdated {toConnection :: PendingContactConnection}
|
||||
| CRContactPrefsUpdated {fromContact :: Contact, toContact :: Contact, preferences :: ContactUserPreferences}
|
||||
| CRContactConnecting {contact :: Contact}
|
||||
| CRContactConnected {contact :: Contact, userCustomProfile :: Maybe Profile}
|
||||
| CRContactAnotherClient {contact :: Contact}
|
||||
@@ -448,6 +456,15 @@ instance ToJSON NtfMsgInfo where toEncoding = J.genericToEncoding J.defaultOptio
|
||||
crNtfToken :: (DeviceToken, NtfTknStatus, NotificationsMode) -> ChatResponse
|
||||
crNtfToken (token, status, ntfMode) = CRNtfToken {token, status, ntfMode}
|
||||
|
||||
data SwitchProgress = SwitchProgress
|
||||
{ queueDirection :: QueueDirection,
|
||||
switchPhase :: SwitchPhase,
|
||||
connectionStats :: ConnectionStats
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON SwitchProgress where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data ChatError
|
||||
= ChatError {errorType :: ChatErrorType}
|
||||
| ChatErrorAgent {agentError :: AgentErrorType}
|
||||
|
||||
@@ -32,12 +32,11 @@ import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgErrorType (..), MsgMeta (..))
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgErrorType (..), MsgMeta (..), SwitchPhase (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (MsgBody)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||
|
||||
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
|
||||
deriving (Show, Generic)
|
||||
@@ -524,6 +523,21 @@ sndGroupEventToText = \case
|
||||
SGEUserLeft -> "left"
|
||||
SGEGroupUpdated _ -> "group profile updated"
|
||||
|
||||
rcvConnEventToText :: RcvConnEvent -> Text
|
||||
rcvConnEventToText = \case
|
||||
RCESwitchQueue phase -> case phase of
|
||||
SPCompleted -> "changed address for you"
|
||||
_ -> decodeLatin1 (strEncode phase) <> " changing address for you..."
|
||||
|
||||
sndConnEventToText :: SndConnEvent -> Text
|
||||
sndConnEventToText = \case
|
||||
SCESwitchQueue phase m -> case phase of
|
||||
SPCompleted -> "you changed address" <> forMember m
|
||||
_ -> decodeLatin1 (strEncode phase) <> " changing address" <> forMember m <> "..."
|
||||
where
|
||||
forMember member_ =
|
||||
maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_
|
||||
|
||||
profileToText :: Profile -> Text
|
||||
profileToText Profile {displayName, fullName} = displayName <> optionalFullName displayName fullName
|
||||
|
||||
@@ -542,6 +556,8 @@ data CIContent (d :: MsgDirection) where
|
||||
CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd
|
||||
CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv
|
||||
CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd
|
||||
CIRcvConnEvent :: RcvConnEvent -> CIContent 'MDRcv
|
||||
CISndConnEvent :: SndConnEvent -> 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
|
||||
@@ -604,6 +620,44 @@ instance ToJSON DBSndGroupEvent where
|
||||
toJSON (SGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SGE") v
|
||||
toEncoding (SGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SGE") v
|
||||
|
||||
data RcvConnEvent = RCESwitchQueue {phase :: SwitchPhase}
|
||||
deriving (Show, Generic)
|
||||
|
||||
data SndConnEvent = SCESwitchQueue {phase :: SwitchPhase, member :: Maybe GroupMemberRef}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance FromJSON RcvConnEvent where
|
||||
parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RCE"
|
||||
|
||||
instance ToJSON RcvConnEvent where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RCE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RCE"
|
||||
|
||||
newtype DBRcvConnEvent = RCE RcvConnEvent
|
||||
|
||||
instance FromJSON DBRcvConnEvent where
|
||||
parseJSON v = RCE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RCE") v
|
||||
|
||||
instance ToJSON DBRcvConnEvent where
|
||||
toJSON (RCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RCE") v
|
||||
toEncoding (RCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RCE") v
|
||||
|
||||
instance FromJSON SndConnEvent where
|
||||
parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SCE"
|
||||
|
||||
instance ToJSON SndConnEvent where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SCE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SCE"
|
||||
|
||||
newtype DBSndConnEvent = SCE SndConnEvent
|
||||
|
||||
instance FromJSON DBSndConnEvent where
|
||||
parseJSON v = SCE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "SCE") v
|
||||
|
||||
instance ToJSON DBSndConnEvent where
|
||||
toJSON (SCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SCE") v
|
||||
toEncoding (SCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SCE") v
|
||||
|
||||
newtype DBMsgErrorType = DBME MsgErrorType
|
||||
|
||||
instance FromJSON DBMsgErrorType where
|
||||
@@ -653,6 +707,8 @@ ciContentToText = \case
|
||||
CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole
|
||||
CIRcvGroupEvent event -> rcvGroupEventToText event
|
||||
CISndGroupEvent event -> sndGroupEventToText event
|
||||
CIRcvConnEvent event -> rcvConnEventToText event
|
||||
CISndConnEvent event -> sndConnEventToText event
|
||||
|
||||
msgIntegrityError :: MsgErrorType -> Text
|
||||
msgIntegrityError = \case
|
||||
@@ -670,7 +726,7 @@ msgDirToDeletedContent_ msgDir mode = case msgDir of
|
||||
|
||||
-- platform independent
|
||||
instance ToField (CIContent d) where
|
||||
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode . dbJsonCIContent
|
||||
toField = toField . encodeJSON . dbJsonCIContent
|
||||
|
||||
-- platform specific
|
||||
instance ToJSON (CIContent d) where
|
||||
@@ -686,7 +742,7 @@ instance FromJSON ACIContent where
|
||||
parseJSON = fmap aciContentJSON . J.parseJSON
|
||||
|
||||
-- platform independent
|
||||
instance FromField ACIContent where fromField = fromTextField_ $ fmap aciContentDBJSON . J.decode . LB.fromStrict . encodeUtf8
|
||||
instance FromField ACIContent where fromField = fromTextField_ $ fmap aciContentDBJSON . decodeJSON
|
||||
|
||||
-- platform specific
|
||||
data JSONCIContent
|
||||
@@ -701,6 +757,8 @@ data JSONCIContent
|
||||
| JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| JCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent}
|
||||
| JCISndGroupEvent {sndGroupEvent :: SndGroupEvent}
|
||||
| JCIRcvConnEvent {rcvConnEvent :: RcvConnEvent}
|
||||
| JCISndConnEvent {sndConnEvent :: SndConnEvent}
|
||||
deriving (Generic)
|
||||
|
||||
instance FromJSON JSONCIContent where
|
||||
@@ -723,6 +781,8 @@ jsonCIContent = \case
|
||||
CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole}
|
||||
CIRcvGroupEvent rcvGroupEvent -> JCIRcvGroupEvent {rcvGroupEvent}
|
||||
CISndGroupEvent sndGroupEvent -> JCISndGroupEvent {sndGroupEvent}
|
||||
CIRcvConnEvent rcvConnEvent -> JCIRcvConnEvent {rcvConnEvent}
|
||||
CISndConnEvent sndConnEvent -> JCISndConnEvent {sndConnEvent}
|
||||
|
||||
aciContentJSON :: JSONCIContent -> ACIContent
|
||||
aciContentJSON = \case
|
||||
@@ -737,6 +797,8 @@ aciContentJSON = \case
|
||||
JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
||||
JCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent
|
||||
JCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent
|
||||
JCIRcvConnEvent {rcvConnEvent} -> ACIContent SMDRcv $ CIRcvConnEvent rcvConnEvent
|
||||
JCISndConnEvent {sndConnEvent} -> ACIContent SMDSnd $ CISndConnEvent sndConnEvent
|
||||
|
||||
-- platform independent
|
||||
data DBJSONCIContent
|
||||
@@ -751,6 +813,8 @@ data DBJSONCIContent
|
||||
| DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent}
|
||||
| DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent}
|
||||
| DBJCIRcvConnEvent {rcvConnEvent :: DBRcvConnEvent}
|
||||
| DBJCISndConnEvent {sndConnEvent :: DBSndConnEvent}
|
||||
deriving (Generic)
|
||||
|
||||
instance FromJSON DBJSONCIContent where
|
||||
@@ -773,6 +837,8 @@ dbJsonCIContent = \case
|
||||
CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole}
|
||||
CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge
|
||||
CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge
|
||||
CIRcvConnEvent rce -> DBJCIRcvConnEvent $ RCE rce
|
||||
CISndConnEvent sce -> DBJCISndConnEvent $ SCE sce
|
||||
|
||||
aciContentDBJSON :: DBJSONCIContent -> ACIContent
|
||||
aciContentDBJSON = \case
|
||||
@@ -787,6 +853,8 @@ aciContentDBJSON = \case
|
||||
DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
||||
DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge
|
||||
DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge
|
||||
DBJCIRcvConnEvent (RCE rce) -> ACIContent SMDRcv $ CIRcvConnEvent rce
|
||||
DBJCISndConnEvent (SCE sce) -> ACIContent SMDSnd $ CISndConnEvent sce
|
||||
|
||||
data CICallStatus
|
||||
= CISCallPending
|
||||
|
||||
@@ -41,7 +41,7 @@ CREATE TABLE display_names (
|
||||
|
||||
CREATE TABLE contacts (
|
||||
contact_id INTEGER PRIMARY KEY,
|
||||
contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- NULL if it's an incognito profile
|
||||
contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
local_display_name TEXT NOT NULL,
|
||||
is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user
|
||||
@@ -223,7 +223,7 @@ CREATE TABLE contact_requests (
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
agent_invitation_id BLOB NOT NULL,
|
||||
contact_profile_id INTEGER REFERENCES contact_profiles
|
||||
ON DELETE SET NULL -- NULL if it's an incognito profile
|
||||
ON DELETE SET NULL
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
local_display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20221025_chat_settings where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20221025_chat_settings :: Query
|
||||
m20221025_chat_settings =
|
||||
[sql|
|
||||
PRAGMA ignore_check_constraints=ON;
|
||||
|
||||
ALTER TABLE group_profiles ADD COLUMN preferences TEXT;
|
||||
|
||||
ALTER TABLE contact_profiles ADD COLUMN preferences TEXT;
|
||||
|
||||
ALTER TABLE contacts ADD COLUMN user_preferences TEXT DEFAULT '{}' CHECK (user_preferences NOT NULL);
|
||||
UPDATE contacts SET user_preferences = '{}';
|
||||
|
||||
PRAGMA ignore_check_constraints=OFF;
|
||||
|]
|
||||
@@ -0,0 +1,14 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20221029_group_link_id where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20221029_group_link_id :: Query
|
||||
m20221029_group_link_id =
|
||||
[sql|
|
||||
ALTER TABLE user_contact_links ADD COLUMN group_link_id BLOB;
|
||||
|
||||
ALTER TABLE connections ADD COLUMN group_link_id BLOB;
|
||||
|]
|
||||
@@ -15,7 +15,8 @@ CREATE TABLE contact_profiles(
|
||||
image TEXT,
|
||||
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE,
|
||||
incognito INTEGER,
|
||||
local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL)
|
||||
local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL),
|
||||
preferences TEXT
|
||||
);
|
||||
CREATE INDEX contact_profiles_index ON contact_profiles(
|
||||
display_name,
|
||||
@@ -47,10 +48,10 @@ CREATE TABLE display_names(
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE contacts(
|
||||
contact_id INTEGER PRIMARY KEY,
|
||||
contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- NULL if it's an incognito profile
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
local_display_name TEXT NOT NULL,
|
||||
is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user
|
||||
contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
local_display_name TEXT NOT NULL,
|
||||
is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user
|
||||
via_group INTEGER REFERENCES groups(group_id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
@@ -58,6 +59,7 @@ is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user
|
||||
enable_ntfs INTEGER,
|
||||
unread_chat INTEGER DEFAULT 0 CHECK(unread_chat NOT NULL),
|
||||
contact_used INTEGER DEFAULT 0 CHECK(contact_used NOT NULL),
|
||||
user_preferences TEXT DEFAULT '{}' CHECK(user_preferences NOT NULL),
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE CASCADE
|
||||
@@ -113,7 +115,8 @@ CREATE TABLE group_profiles(
|
||||
created_at TEXT CHECK(created_at NOT NULL),
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
image TEXT,
|
||||
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE
|
||||
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE,
|
||||
preferences TEXT
|
||||
);
|
||||
CREATE TABLE groups(
|
||||
group_id INTEGER PRIMARY KEY, -- local group ID
|
||||
@@ -254,6 +257,7 @@ CREATE TABLE connections(
|
||||
conn_req_inv BLOB,
|
||||
local_alias DEFAULT '' CHECK(local_alias NOT NULL),
|
||||
via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL),
|
||||
group_link_id BLOB,
|
||||
FOREIGN KEY(snd_file_id, connection_id)
|
||||
REFERENCES snd_files(file_id, connection_id)
|
||||
ON DELETE CASCADE
|
||||
@@ -270,6 +274,7 @@ CREATE TABLE user_contact_links(
|
||||
auto_reply_msg_content TEXT DEFAULT NULL,
|
||||
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
|
||||
auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL),
|
||||
group_link_id BLOB,
|
||||
UNIQUE(user_id, local_display_name)
|
||||
);
|
||||
CREATE TABLE contact_requests(
|
||||
@@ -278,18 +283,20 @@ CREATE TABLE contact_requests(
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
agent_invitation_id BLOB NOT NULL,
|
||||
contact_profile_id INTEGER REFERENCES contact_profiles
|
||||
ON DELETE SET NULL -- NULL if it's an incognito profile
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
local_display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB,
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
UNIQUE(user_id, local_display_name),
|
||||
UNIQUE(user_id, contact_profile_id)
|
||||
ON DELETE SET NULL
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
local_display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
xcontact_id BLOB,
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
UNIQUE(user_id, local_display_name),
|
||||
UNIQUE(user_id, contact_profile_id)
|
||||
);
|
||||
CREATE TABLE messages(
|
||||
message_id INTEGER PRIMARY KEY,
|
||||
|
||||
@@ -31,13 +31,12 @@ import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations), createAgentStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore)
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (CorrId (..))
|
||||
import Simplex.Messaging.Util (catchAll)
|
||||
import Simplex.Messaging.Util (catchAll, safeDecodeUtf8)
|
||||
import System.Timeout (timeout)
|
||||
|
||||
foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
|
||||
@@ -10,7 +10,7 @@ generateRandomProfile :: IO Profile
|
||||
generateRandomProfile = do
|
||||
adjective <- pick adjectives
|
||||
noun <- pickNoun adjective 2
|
||||
pure $ Profile {displayName = adjective <> noun, fullName = "", image = Nothing}
|
||||
pure $ Profile {displayName = adjective <> noun, fullName = "", image = Nothing, preferences = Nothing}
|
||||
where
|
||||
pick :: [a] -> IO a
|
||||
pick xs = (xs !!) <$> randomRIO (0, length xs - 1)
|
||||
|
||||
@@ -39,11 +39,10 @@ import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (fromTextField_, fstToLower, parseAll, sumTypeJSON)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||
|
||||
data ConnectionEntity
|
||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||
@@ -392,10 +391,10 @@ instance ToJSON MsgContent where
|
||||
MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t
|
||||
|
||||
instance ToField MsgContent where
|
||||
toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode
|
||||
toField = toField . encodeJSON
|
||||
|
||||
instance FromField MsgContent where
|
||||
fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8
|
||||
fromField = fromTextField_ decodeJSON
|
||||
|
||||
data CMEventTag (e :: MsgEncoding) where
|
||||
XMsgNew_ :: CMEventTag 'Json
|
||||
|
||||
+256
-155
@@ -40,6 +40,7 @@ module Simplex.Chat.Store
|
||||
getContactIdByName,
|
||||
updateUserProfile,
|
||||
updateContactProfile,
|
||||
updateContactUserPreferences,
|
||||
updateContactAlias,
|
||||
updateContactConnectionAlias,
|
||||
updateContactUsed,
|
||||
@@ -57,6 +58,8 @@ module Simplex.Chat.Store
|
||||
getGroupLinkConnection,
|
||||
deleteGroupLink,
|
||||
getGroupLink,
|
||||
getGroupLinkId,
|
||||
getConnectionGroupLinkId,
|
||||
createOrUpdateContactRequest,
|
||||
getContactRequest,
|
||||
getContactRequestIdByName,
|
||||
@@ -83,6 +86,7 @@ module Simplex.Chat.Store
|
||||
getGroupInfoByName,
|
||||
getGroupMember,
|
||||
getGroupMembers,
|
||||
getGroupMembersForExpiration,
|
||||
deleteGroupConnectionsAndFiles,
|
||||
deleteGroupItemsAndMembers,
|
||||
deleteGroup,
|
||||
@@ -95,9 +99,11 @@ module Simplex.Chat.Store
|
||||
setNewContactMemberConnRequest,
|
||||
getMemberInvitation,
|
||||
createMemberConnection,
|
||||
createMemberConnectionAsync,
|
||||
updateGroupMemberStatus,
|
||||
updateGroupMemberStatusById,
|
||||
createNewGroupMember,
|
||||
checkGroupMemberHasItems,
|
||||
deleteGroupMember,
|
||||
deleteGroupMemberConnection,
|
||||
updateGroupMemberRole,
|
||||
@@ -241,7 +247,7 @@ import Data.Either (rights)
|
||||
import Data.Function (on)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find, sortBy, sortOn)
|
||||
import Data.List (sortBy, sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Maybe (fromMaybe, isJust, isNothing, listToMaybe)
|
||||
import Data.Ord (Down (..))
|
||||
@@ -288,6 +294,8 @@ import Simplex.Chat.Migrations.M20221012_inline_files
|
||||
import Simplex.Chat.Migrations.M20221019_unread_chat
|
||||
import Simplex.Chat.Migrations.M20221021_auto_accept__group_links
|
||||
import Simplex.Chat.Migrations.M20221024_contact_used
|
||||
import Simplex.Chat.Migrations.M20221025_chat_settings
|
||||
import Simplex.Chat.Migrations.M20221029_group_link_id
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..))
|
||||
@@ -332,7 +340,9 @@ schemaMigrations =
|
||||
("20221012_inline_files", m20221012_inline_files),
|
||||
("20221019_unread_chat", m20221019_unread_chat),
|
||||
("20221021_auto_accept__group_links", m20221021_auto_accept__group_links),
|
||||
("20221024_contact_used", m20221024_contact_used)
|
||||
("20221024_contact_used", m20221024_contact_used),
|
||||
("20221025_chat_settings", m20221025_chat_settings),
|
||||
("20221029_group_link_id", m20221029_group_link_id)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -362,7 +372,7 @@ insertedRowId :: DB.Connection -> IO Int64
|
||||
insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()"
|
||||
|
||||
createUser :: DB.Connection -> Profile -> Bool -> ExceptT StoreError IO User
|
||||
createUser db Profile {displayName, fullName, image} activeUser =
|
||||
createUser db Profile {displayName, fullName, image, preferences = userPreferences} activeUser =
|
||||
checkConstraint SEDuplicateName . liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
@@ -376,8 +386,8 @@ createUser db Profile {displayName, fullName, image} activeUser =
|
||||
(displayName, displayName, userId, currentTs, currentTs)
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, currentTs, currentTs)
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, userPreferences, currentTs, currentTs)
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
@@ -385,7 +395,7 @@ createUser db Profile {displayName, fullName, image} activeUser =
|
||||
(profileId, displayName, userId, True, currentTs, currentTs)
|
||||
contactId <- insertedRowId db
|
||||
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId)
|
||||
pure $ toUser (userId, contactId, profileId, activeUser, displayName, fullName, image)
|
||||
pure $ toUser (userId, contactId, profileId, activeUser, displayName, fullName, image, userPreferences)
|
||||
|
||||
getUsers :: DB.Connection -> IO [User]
|
||||
getUsers db =
|
||||
@@ -393,15 +403,15 @@ getUsers db =
|
||||
<$> DB.query_
|
||||
db
|
||||
[sql|
|
||||
SELECT u.user_id, u.contact_id, p.contact_profile_id, u.active_user, u.local_display_name, p.full_name, p.image
|
||||
SELECT u.user_id, u.contact_id, p.contact_profile_id, u.active_user, u.local_display_name, p.full_name, p.image, p.preferences
|
||||
FROM users u
|
||||
JOIN contacts c ON u.contact_id = c.contact_id
|
||||
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
|
||||
|]
|
||||
|
||||
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, localAlias = ""}
|
||||
toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences) -> User
|
||||
toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, preferences = userPreferences, localAlias = ""}
|
||||
in User {userId, userContactId, localDisplayName = displayName, profile, activeUser}
|
||||
|
||||
setActiveUser :: DB.Connection -> UserId -> IO ()
|
||||
@@ -409,8 +419,8 @@ setActiveUser db userId = do
|
||||
DB.execute_ db "UPDATE users SET active_user = 0"
|
||||
DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId)
|
||||
|
||||
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> IO PendingContactConnection
|
||||
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile = do
|
||||
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> IO PendingContactConnection
|
||||
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId = do
|
||||
createdAt <- getCurrentTime
|
||||
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
|
||||
let pccConnStatus = ConnJoined
|
||||
@@ -419,12 +429,12 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile = do
|
||||
[sql|
|
||||
INSERT INTO connections (
|
||||
user_id, agent_conn_id, conn_status, conn_type,
|
||||
via_contact_uri_hash, xcontact_id, custom_user_profile_id, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?)
|
||||
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
(userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId, customUserProfileId, createdAt, createdAt)
|
||||
((userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt))
|
||||
pccConnId <- insertedRowId db
|
||||
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
|
||||
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
|
||||
|
||||
getConnReqContactXContactId :: DB.Connection -> UserId -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId)
|
||||
getConnReqContactXContactId db userId cReqHash = do
|
||||
@@ -440,7 +450,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, cp.local_alias, ct.contact_used, 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.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, 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.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
@@ -472,7 +482,7 @@ createDirectConnection db userId acId cReq pccConnStatus incognitoProfile = do
|
||||
|]
|
||||
(userId, acId, cReq, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt)
|
||||
pccConnId <- insertedRowId db
|
||||
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt}
|
||||
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt}
|
||||
|
||||
createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64
|
||||
createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do
|
||||
@@ -491,14 +501,15 @@ getProfileById db userId profileId =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT display_name, full_name, image, local_alias
|
||||
FROM contact_profiles
|
||||
WHERE user_id = ? AND contact_profile_id = ?
|
||||
SELECT cp.display_name, cp.full_name, cp.image, cp.local_alias, cp.preferences -- , ct.user_preferences
|
||||
FROM contact_profiles cp
|
||||
-- JOIN contacts ct ON cp.contact_profile_id = ct.contact_profile_id
|
||||
WHERE cp.user_id = ? AND cp.contact_profile_id = ?
|
||||
|]
|
||||
(userId, profileId)
|
||||
where
|
||||
toProfile :: (ContactName, Text, Maybe ImageData, LocalAlias) -> LocalProfile
|
||||
toProfile (displayName, fullName, image, localAlias) = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
toProfile :: (ContactName, Text, Maybe ImageData, LocalAlias, Maybe Preferences) -> LocalProfile
|
||||
toProfile (displayName, fullName, image, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, preferences, 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
|
||||
@@ -525,15 +536,15 @@ createDirectContact :: DB.Connection -> UserId -> Connection -> Profile -> Excep
|
||||
createDirectContact db userId activeConn@Connection {connId, localAlias} profile = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
(localDisplayName, contactId, profileId) <- createContact_ db userId connId profile localAlias Nothing createdAt
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile localAlias, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, createdAt, updatedAt = createdAt}
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile localAlias, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences = emptyChatPrefs, createdAt, updatedAt = createdAt}
|
||||
|
||||
createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId)
|
||||
createContact_ db userId connId Profile {displayName, fullName, image} localAlias viaGroup currentTs =
|
||||
createContact_ db userId connId Profile {displayName, fullName, image, preferences} localAlias viaGroup currentTs =
|
||||
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, local_alias, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, localAlias, currentTs, currentTs)
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, local_alias, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, localAlias, preferences, currentTs, currentTs)
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
@@ -558,8 +569,8 @@ deleteContactConnectionsAndFiles db userId Contact {contactId} = do
|
||||
(userId, contactId)
|
||||
DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
|
||||
deleteContact :: DB.Connection -> UserId -> Contact -> IO ()
|
||||
deleteContact db userId Contact {contactId, localDisplayName} = do
|
||||
deleteContact :: DB.Connection -> User -> Contact -> IO ()
|
||||
deleteContact db user@User {userId} Contact {contactId, localDisplayName, activeConn = Connection {customUserProfileId}} = do
|
||||
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
ctMember :: (Maybe ContactId) <- maybeFirstRow fromOnly $ DB.query db "SELECT contact_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId)
|
||||
if isNothing ctMember
|
||||
@@ -570,6 +581,25 @@ deleteContact db userId Contact {contactId, localDisplayName} = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE group_members SET contact_id = NULL, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId)
|
||||
DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
forM_ customUserProfileId $ \profileId -> deleteUnusedIncognitoProfileById_ db user profileId
|
||||
|
||||
deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO ()
|
||||
deleteUnusedIncognitoProfileById_ db User {userId} profile_id =
|
||||
DB.executeNamed
|
||||
db
|
||||
[sql|
|
||||
DELETE FROM contact_profiles
|
||||
WHERE user_id = :user_id AND contact_profile_id = :profile_id AND incognito = 1
|
||||
AND 1 NOT IN (
|
||||
SELECT 1 FROM connections
|
||||
WHERE user_id = :user_id AND custom_user_profile_id = :profile_id LIMIT 1
|
||||
)
|
||||
AND 1 NOT IN (
|
||||
SELECT 1 FROM group_members
|
||||
WHERE user_id = :user_id AND member_profile_id = :profile_id LIMIT 1
|
||||
)
|
||||
|]
|
||||
[":user_id" := userId, ":profile_id" := profile_id]
|
||||
|
||||
deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO ()
|
||||
deleteContactProfile_ db userId contactId =
|
||||
@@ -611,6 +641,14 @@ updateContactProfile db userId c@Contact {contactId, localDisplayName, profile =
|
||||
updateContact_ db userId contactId localDisplayName ldn currentTs
|
||||
pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p' localAlias}
|
||||
|
||||
updateContactUserPreferences :: DB.Connection -> UserId -> Int64 -> Preferences -> IO ()
|
||||
updateContactUserPreferences db userId contactId userPreferences = do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE contacts SET user_preferences = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?"
|
||||
(userPreferences, updatedAt, userId, contactId)
|
||||
|
||||
updateContactAlias :: DB.Connection -> UserId -> Contact -> LocalAlias -> IO Contact
|
||||
updateContactAlias db userId c@Contact {profile = lp@LocalProfile {profileId}} localAlias = do
|
||||
updatedAt <- getCurrentTime
|
||||
@@ -658,15 +696,15 @@ updateContactProfile_ db userId profileId profile = do
|
||||
updateContactProfile_' db userId profileId profile currentTs
|
||||
|
||||
updateContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO ()
|
||||
updateContactProfile_' db userId profileId Profile {displayName, fullName, image} updatedAt = do
|
||||
updateContactProfile_' db userId profileId Profile {displayName, fullName, image, preferences} updatedAt = do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET display_name = ?, full_name = ?, image = ?, updated_at = ?
|
||||
SET display_name = ?, full_name = ?, image = ?, preferences = ?, updated_at = ?
|
||||
WHERE user_id = ? AND contact_profile_id = ?
|
||||
|]
|
||||
(displayName, fullName, image, updatedAt, userId, profileId)
|
||||
(displayName, fullName, image, preferences, updatedAt, userId, profileId)
|
||||
|
||||
updateContact_ :: DB.Connection -> UserId -> Int64 -> ContactName -> ContactName -> UTCTime -> IO ()
|
||||
updateContact_ db userId contactId displayName newName updatedAt = do
|
||||
@@ -680,22 +718,22 @@ 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, LocalAlias, Bool, Maybe Bool, UTCTime, UTCTime)
|
||||
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, LocalAlias, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime)
|
||||
|
||||
toContact :: ContactRow :. ConnectionRow -> Contact
|
||||
toContact ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
toContact (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias}
|
||||
activeConn = toConnection connRow
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, createdAt, updatedAt}
|
||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt}
|
||||
|
||||
toContactOrError :: ContactRow :. MaybeConnectionRow -> Either StoreError Contact
|
||||
toContactOrError ((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
toContactOrError (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, localAlias, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias}
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in case toMaybeConnection connRow of
|
||||
Just activeConn ->
|
||||
Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, createdAt, updatedAt}
|
||||
Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt}
|
||||
_ -> Left $ SEContactNotReady localDisplayName
|
||||
|
||||
-- TODO return the last connection that is ready, not any last connection
|
||||
@@ -862,16 +900,16 @@ updateUserAddressAutoAccept db userId autoAccept = do
|
||||
Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply)
|
||||
_ -> (False, False, Nothing)
|
||||
|
||||
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> ExceptT StoreError IO ()
|
||||
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq =
|
||||
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> ExceptT StoreError IO ()
|
||||
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId =
|
||||
checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO user_contact_links (user_id, group_id, local_display_name, conn_req_contact, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(userId, groupId, "group_link_" <> localDisplayName, cReq, True, currentTs, currentTs)
|
||||
groupLinkId <- insertedRowId db
|
||||
void $ createConnection_ db userId ConnUserContact (Just groupLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
|
||||
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, True, currentTs, currentTs)
|
||||
userContactLinkId <- insertedRowId db
|
||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
|
||||
|
||||
getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection
|
||||
getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
|
||||
@@ -930,10 +968,20 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do
|
||||
getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO ConnReqContact
|
||||
getGroupLink db User {userId} gInfo@GroupInfo {groupId} =
|
||||
ExceptT . firstRow fromOnly (SEGroupLinkNotFound gInfo) $
|
||||
DB.query db "SELECT conn_req_contact FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||
DB.query db "SELECT conn_req_contact FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
|
||||
|
||||
getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId)
|
||||
getGroupLinkId db User {userId} GroupInfo {groupId} =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
|
||||
|
||||
getConnectionGroupLinkId :: DB.Connection -> User -> Int64 -> IO (Maybe GroupLinkId)
|
||||
getConnectionGroupLinkId db User {userId} connId =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT group_link_id FROM connections WHERE user_id = ? AND connection_id = ? LIMIT 1" (userId, connId)
|
||||
|
||||
createOrUpdateContactRequest :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
|
||||
createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image} xContactId_ =
|
||||
createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ =
|
||||
liftIO (maybeM getContact' xContactId_) >>= \case
|
||||
Just contact -> pure $ CORContact contact
|
||||
Nothing -> CORRequest <$> createOrUpdate_
|
||||
@@ -955,8 +1003,8 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN
|
||||
createContactRequest_ currentTs ldn = do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, currentTs, currentTs)
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, preferences, currentTs, currentTs)
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
@@ -975,7 +1023,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, cp.local_alias, ct.contact_used, 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.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, 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.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
@@ -995,7 +1043,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN
|
||||
[sql|
|
||||
SELECT
|
||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, cr.created_at, cr.updated_at
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
|
||||
FROM contact_requests cr
|
||||
JOIN connections c USING (user_contact_link_id)
|
||||
JOIN contact_profiles p USING (contact_profile_id)
|
||||
@@ -1041,7 +1089,7 @@ getContactRequest db userId contactRequestId =
|
||||
[sql|
|
||||
SELECT
|
||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, cr.created_at, cr.updated_at
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
|
||||
FROM contact_requests cr
|
||||
JOIN connections c USING (user_contact_link_id)
|
||||
JOIN contact_profiles p USING (contact_profile_id)
|
||||
@@ -1050,11 +1098,11 @@ getContactRequest db userId contactRequestId =
|
||||
|]
|
||||
(userId, contactRequestId)
|
||||
|
||||
type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe XContactId, UTCTime, UTCTime)
|
||||
type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime)
|
||||
|
||||
toContactRequest :: ContactRequestRow -> UserContactRequest
|
||||
toContactRequest (contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, xContactId, createdAt, updatedAt) = do
|
||||
let profile = Profile {displayName, fullName, image}
|
||||
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image) :. (xContactId, preferences, createdAt, updatedAt)) = do
|
||||
let profile = Profile {displayName, fullName, image, preferences}
|
||||
in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt}
|
||||
|
||||
getContactRequestIdByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Int64
|
||||
@@ -1087,20 +1135,21 @@ deleteContactRequest db userId contactRequestId = do
|
||||
(userId, userId, contactRequestId)
|
||||
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
|
||||
|
||||
createAcceptedContact :: DB.Connection -> UserId -> ConnId -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact
|
||||
createAcceptedContact db userId agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do
|
||||
createAcceptedContact :: DB.Connection -> User -> ConnId -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact
|
||||
createAcceptedContact db User {userId, profile = LocalProfile {preferences}} agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do
|
||||
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
|
||||
createdAt <- getCurrentTime
|
||||
customUserProfileId <- forM incognitoProfile $ \case
|
||||
NewIncognito p -> createIncognitoProfile_ db userId createdAt p
|
||||
ExistingIncognito LocalProfile {profileId = pId} -> pure pId
|
||||
let contactUserPrefs = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?,?)"
|
||||
(userId, localDisplayName, profileId, True, createdAt, createdAt, xContactId)
|
||||
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, xcontact_id) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(userId, localDisplayName, profileId, True, contactUserPrefs, 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, contactUsed = False, chatSettings = defaultChatSettings, createdAt = createdAt, updatedAt = createdAt}
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences = contactUserPrefs, createdAt = createdAt, updatedAt = createdAt}
|
||||
|
||||
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
|
||||
getLiveSndFileTransfers db User {userId} = do
|
||||
@@ -1154,7 +1203,7 @@ getPendingContactConnections db User {userId} = do
|
||||
<$> DB.queryNamed
|
||||
db
|
||||
[sql|
|
||||
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
|
||||
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
|
||||
FROM connections
|
||||
WHERE user_id = :user_id
|
||||
AND conn_type = :conn_type
|
||||
@@ -1324,6 +1373,10 @@ mergeContactRecords db userId Contact {contactId = toContactId} Contact {contact
|
||||
db
|
||||
"UPDATE group_members SET invited_by = ?, updated_at = ? WHERE invited_by = ? AND user_id = ?"
|
||||
(toContactId, currentTs, fromContactId, userId)
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE chat_items SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?"
|
||||
(toContactId, currentTs, fromContactId, userId)
|
||||
DB.executeNamed
|
||||
db
|
||||
[sql|
|
||||
@@ -1378,17 +1431,17 @@ 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, p.local_alias, c.via_group, c.contact_used, 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.contact_used, c.enable_ntfs, p.preferences, c.user_preferences, 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, LocalAlias, Maybe Int64, Bool, Maybe Bool, UTCTime, UTCTime)] -> Either StoreError Contact
|
||||
toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, contactUsed, enableNtfs_, createdAt, updatedAt)] =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime)] -> Either StoreError Contact
|
||||
toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)] =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias}
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, createdAt, updatedAt}
|
||||
in Right $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt}
|
||||
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
|
||||
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
||||
getGroupAndMember_ groupMemberId c = ExceptT $ do
|
||||
@@ -1398,15 +1451,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.host_conn_custom_user_profile_id, 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, gp.preferences, 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.local_alias,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias, pu.preferences,
|
||||
-- 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, p.local_alias
|
||||
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, p.preferences
|
||||
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
|
||||
@@ -1499,15 +1552,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.host_conn_custom_user_profile_id, 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, gp.preferences, 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.local_alias,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias, pu.preferences,
|
||||
-- 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, p.local_alias,
|
||||
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, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, 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
|
||||
@@ -1539,14 +1592,14 @@ updateConnectionStatus db Connection {connId} connStatus = do
|
||||
-- | creates completely new group with a single member - the current user
|
||||
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
|
||||
let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile
|
||||
currentTs <- getCurrentTime
|
||||
withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
|
||||
groupId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO group_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, currentTs, currentTs)
|
||||
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
@@ -1559,45 +1612,53 @@ createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do
|
||||
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 ProfileId -> ExceptT StoreError IO GroupInfo
|
||||
createGroupInvitation :: DB.Connection -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId)
|
||||
createGroupInvitation db user@User {userId} contact@Contact {contactId, activeConn = Connection {customUserProfileId}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do
|
||||
liftIO getInvitationGroupId_ >>= \case
|
||||
Nothing -> createGroupInvitation_
|
||||
Just gId -> do
|
||||
gInfo@GroupInfo {membership, groupProfile = p'} <- getGroupInfo db user gId
|
||||
hostId <- getHostMemberId_ db user gId
|
||||
let GroupMember {groupMemberId, memberId, memberRole} = membership
|
||||
MemberIdRole {memberId = memberId', memberRole = memberRole'} = invitedMember
|
||||
liftIO . when (memberId /= memberId' || memberRole /= memberRole') $
|
||||
DB.execute db "UPDATE group_members SET member_id = ?, member_role = ? WHERE group_member_id = ?" (memberId', memberRole', groupMemberId)
|
||||
if p' == groupProfile
|
||||
then pure gInfo
|
||||
else updateGroupProfile db user gInfo groupProfile
|
||||
gInfo' <-
|
||||
if p' == groupProfile
|
||||
then pure gInfo
|
||||
else updateGroupProfile db user gInfo groupProfile
|
||||
pure (gInfo', hostId)
|
||||
where
|
||||
getInvitationGroupId_ :: IO (Maybe Int64)
|
||||
getInvitationGroupId_ =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
|
||||
createGroupInvitation_ :: ExceptT StoreError IO GroupInfo
|
||||
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId)
|
||||
createGroupInvitation_ = do
|
||||
let GroupProfile {displayName, fullName, image} = groupProfile
|
||||
let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile
|
||||
ExceptT $
|
||||
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
groupId <- liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO group_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, currentTs, currentTs)
|
||||
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
|
||||
profileId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
"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 Nothing currentTs
|
||||
GroupMember {groupMemberId} <- 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, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs}
|
||||
pure (GroupInfo {groupId, localDisplayName, groupProfile, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs}, groupMemberId)
|
||||
|
||||
getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
|
||||
getHostMemberId_ db User {userId} groupId =
|
||||
ExceptT . firstRow fromOnly (SEHostMemberIdNotFound groupId) $
|
||||
DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_category = ?" (userId, groupId, GCHostMember)
|
||||
|
||||
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
|
||||
@@ -1676,9 +1737,7 @@ deleteGroupItemsAndMembers :: DB.Connection -> User -> GroupInfo -> [GroupMember
|
||||
deleteGroupItemsAndMembers db user@User {userId} GroupInfo {groupId} members = do
|
||||
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||
DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||
forM_ members $ \m@GroupMember {groupMemberId, memberContactId, memberContactProfileId} -> unless (isJust memberContactId) $ do
|
||||
sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId)
|
||||
unless (isJust sameProfileMember) $ deleteMemberProfileAndName_ db user m
|
||||
forM_ members $ \m -> cleanupMemberContactAndProfile_ db user m
|
||||
|
||||
deleteGroup :: DB.Connection -> User -> GroupInfo -> IO ()
|
||||
deleteGroup db User {userId} GroupInfo {groupId, localDisplayName} = do
|
||||
@@ -1711,9 +1770,9 @@ 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.host_conn_custom_user_profile_id, g.enable_ntfs, g.created_at, g.updated_at,
|
||||
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, gp.preferences, 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
|
||||
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, pu.preferences
|
||||
FROM groups g
|
||||
JOIN group_profiles gp USING (group_profile_id)
|
||||
JOIN group_members mu USING (group_id)
|
||||
@@ -1727,13 +1786,13 @@ getGroupInfoByName db user gName = do
|
||||
gId <- getGroupIdByName db user gName
|
||||
getGroupInfo db user gId
|
||||
|
||||
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, UTCTime, UTCTime) :. GroupMemberRow
|
||||
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe GroupPreferences, UTCTime, UTCTime) :. GroupMemberRow
|
||||
|
||||
toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo
|
||||
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, hostConnCustomUserProfileId, enableNtfs_, createdAt, updatedAt) :. userMemberRow) =
|
||||
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, hostConnCustomUserProfileId, enableNtfs_, groupPreferences, createdAt, updatedAt) :. userMemberRow) =
|
||||
let membership = toGroupMember userContactId userMemberRow
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image}, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
|
||||
in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image, groupPreferences}, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
|
||||
|
||||
getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
|
||||
getGroupMember db user@User {userId} groupId groupMemberId =
|
||||
@@ -1743,7 +1802,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, p.local_alias,
|
||||
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, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, 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
|
||||
@@ -1765,7 +1824,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, p.local_alias,
|
||||
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, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, 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
|
||||
@@ -1779,19 +1838,44 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
|]
|
||||
(groupId, userId, userContactId)
|
||||
|
||||
getGroupMembersForExpiration :: DB.Connection -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
map (toContactMember user)
|
||||
<$> DB.query
|
||||
db
|
||||
[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, p.local_alias,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, 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
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
LEFT JOIN connections c ON c.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.group_member_id = m.group_member_id
|
||||
)
|
||||
WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)
|
||||
AND m.member_status IN (?, ?, ?)
|
||||
AND m.group_member_id NOT IN (
|
||||
SELECT DISTINCT group_member_id FROM chat_items
|
||||
)
|
||||
|]
|
||||
(groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
|
||||
|
||||
toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
|
||||
toContactMember User {userContactId} (memberRow :. connRow) =
|
||||
(toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow}
|
||||
|
||||
-- TODO no need to load all members to find the member who invited the user,
|
||||
-- instead of findFromContact there could be a query
|
||||
getGroupInvitation :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation
|
||||
getGroupInvitation db user groupId = do
|
||||
cReq <- getConnRec_ user
|
||||
Group groupInfo@GroupInfo {membership} members <- getGroup db user groupId
|
||||
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
|
||||
case (cReq, findFromContact (invitedBy membership) members) of
|
||||
(Just connRequest, Just fromMember) ->
|
||||
getGroupInvitation db user groupId =
|
||||
getConnRec_ user >>= \case
|
||||
Just connRequest -> do
|
||||
groupInfo@GroupInfo {membership} <- getGroupInfo db user groupId
|
||||
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
|
||||
hostId <- getHostMemberId_ db user groupId
|
||||
fromMember <- getGroupMember db user groupId hostId
|
||||
pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo}
|
||||
_ -> throwError SEGroupInvitationNotFound
|
||||
where
|
||||
@@ -1799,24 +1883,21 @@ getGroupInvitation db user groupId = do
|
||||
getConnRec_ User {userId} = ExceptT $ do
|
||||
firstRow fromOnly (SEGroupNotFound groupId) $
|
||||
DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ?" (groupId, userId)
|
||||
findFromContact :: InvitedBy -> [GroupMember] -> Maybe GroupMember
|
||||
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, LocalAlias))
|
||||
type GroupMemberRow = ((Int64, Int64, MemberId, GroupMemberRole, GroupMemberCategory, GroupMemberStatus) :. (Maybe Int64, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, LocalAlias, Maybe Preferences))
|
||||
|
||||
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))
|
||||
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, Maybe Preferences))
|
||||
|
||||
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
|
||||
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}
|
||||
toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias, preferences)) =
|
||||
let memberProfile = LocalProfile {profileId, displayName, fullName, image, preferences, 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 localAlias)) =
|
||||
Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias))
|
||||
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, contactPreferences)) =
|
||||
Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, localAlias, contactPreferences))
|
||||
toMaybeGroupMember _ _ = Nothing
|
||||
|
||||
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember
|
||||
@@ -1889,7 +1970,7 @@ getContactViaMember db User {userId} GroupMember {groupMemberId} =
|
||||
[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, cp.local_alias, ct.contact_used, 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.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, 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.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
@@ -1920,6 +2001,12 @@ createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do
|
||||
currentTs <- getCurrentTime
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs
|
||||
|
||||
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> IO ()
|
||||
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) = do
|
||||
currentTs <- getCurrentTime
|
||||
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs
|
||||
setCommandConnId db user cmdId connId
|
||||
|
||||
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
|
||||
updateGroupMemberStatus db userId GroupMember {groupMemberId} = updateGroupMemberStatusById db userId groupMemberId
|
||||
|
||||
@@ -1937,13 +2024,13 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do
|
||||
|
||||
-- | 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 =
|
||||
createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image, preferences}) memCategory memStatus =
|
||||
ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, currentTs, currentTs)
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, userId, preferences, currentTs, currentTs)
|
||||
memProfileId <- insertedRowId db
|
||||
let newMember =
|
||||
NewGroupMember
|
||||
@@ -1986,19 +2073,32 @@ createNewMember_
|
||||
groupMemberId <- insertedRowId db
|
||||
pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile "", memberContactId, memberContactProfileId, activeConn}
|
||||
|
||||
checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId)
|
||||
checkGroupMemberHasItems db User {userId} GroupMember {groupMemberId, groupId} =
|
||||
maybeFirstRow fromOnly $ DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ? LIMIT 1" (userId, groupId, groupMemberId)
|
||||
|
||||
deleteGroupMember :: DB.Connection -> User -> GroupMember -> IO ()
|
||||
deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId, memberContactId, memberContactProfileId} = do
|
||||
deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId} = do
|
||||
deleteGroupMemberConnection db user m
|
||||
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ?" (userId, groupId, groupMemberId)
|
||||
DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId)
|
||||
unless (isJust memberContactId) $ do
|
||||
sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId)
|
||||
unless (isJust sameProfileMember) $ deleteMemberProfileAndName_ db user m
|
||||
cleanupMemberContactAndProfile_ db user m
|
||||
|
||||
deleteMemberProfileAndName_ :: DB.Connection -> User -> GroupMember -> IO ()
|
||||
deleteMemberProfileAndName_ db User {userId} GroupMember {memberContactProfileId, localDisplayName} = do
|
||||
DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId)
|
||||
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
|
||||
-- it's important this function is used in transaction after the actual group_members record is deleted, see checkIncognitoProfileInUse_
|
||||
cleanupMemberContactAndProfile_ :: DB.Connection -> User -> GroupMember -> IO ()
|
||||
cleanupMemberContactAndProfile_ db user@User {userId} m@GroupMember {groupMemberId, localDisplayName, memberContactId, memberContactProfileId, memberProfile = LocalProfile {profileId}} =
|
||||
case memberContactId of
|
||||
Just contactId ->
|
||||
runExceptT (getContact db userId contactId) >>= \case
|
||||
Right ct@Contact {activeConn = Connection {connLevel, viaGroupLink}, contactUsed} ->
|
||||
unless ((connLevel == 0 && not viaGroupLink) || contactUsed) $ deleteContact db user ct
|
||||
_ -> pure ()
|
||||
Nothing -> do
|
||||
sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId)
|
||||
unless (isJust sameProfileMember) $ do
|
||||
DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId)
|
||||
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
|
||||
when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user profileId
|
||||
|
||||
deleteGroupMemberConnection :: DB.Connection -> User -> GroupMember -> IO ()
|
||||
deleteGroupMemberConnection db User {userId} GroupMember {groupMemberId} =
|
||||
@@ -2173,15 +2273,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.host_conn_custom_user_profile_id, 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, gp.preferences, 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.local_alias,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias, pu.preferences,
|
||||
-- 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, p.local_alias,
|
||||
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, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, 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
|
||||
@@ -2213,7 +2313,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, p.local_alias, ct.via_group, ct.contact_used, 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.contact_used, ct.enable_ntfs, p.preferences, ct.user_preferences, 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.via_group_link, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, 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
|
||||
@@ -2229,12 +2329,12 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} =
|
||||
|]
|
||||
(userId, groupMemberId)
|
||||
where
|
||||
toContact' :: (ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Bool, Maybe Bool, UTCTime, UTCTime) :. ConnectionRow -> Contact
|
||||
toContact' ((contactId, profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, contactUsed, enableNtfs_, createdAt, updatedAt) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, LocalAlias, Maybe Int64, Bool, Maybe Bool) :. (Maybe Preferences, Preferences, UTCTime, UTCTime)) :. ConnectionRow -> Contact
|
||||
toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, localAlias, viaGroup, contactUsed, enableNtfs_) :. (preferences, userPreferences, createdAt, updatedAt)) :. connRow) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, preferences, localAlias}
|
||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
|
||||
activeConn = toConnection connRow
|
||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, createdAt, updatedAt}
|
||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, createdAt, updatedAt}
|
||||
|
||||
createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> IO FileTransferMeta
|
||||
createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize = do
|
||||
@@ -3053,7 +3153,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.local_alias
|
||||
p.display_name, p.full_name, p.image, p.local_alias, p.preferences
|
||||
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
|
||||
@@ -3090,7 +3190,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, cp.local_alias, ct.contact_used, 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.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, 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.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at,
|
||||
@@ -3122,7 +3222,7 @@ getDirectChatPreviews_ db User {userId} = do
|
||||
) ChatStats ON ChatStats.contact_id = ct.contact_id
|
||||
LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id
|
||||
WHERE ct.user_id = ?
|
||||
AND ((c.conn_level = 0 AND c.via_group_link = 0) OR i.chat_item_id IS NOT NULL OR ct.contact_used = 1)
|
||||
AND ((c.conn_level = 0 AND c.via_group_link = 0) OR ct.contact_used = 1)
|
||||
AND c.connection_id = (
|
||||
SELECT cc_connection_id FROM (
|
||||
SELECT
|
||||
@@ -3155,11 +3255,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.host_conn_custom_user_profile_id, 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, gp.preferences, 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.local_alias,
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias, pu.preferences,
|
||||
-- ChatStats
|
||||
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat,
|
||||
-- ChatItem
|
||||
@@ -3169,13 +3269,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.local_alias,
|
||||
p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
||||
-- 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.local_alias
|
||||
rp.display_name, rp.full_name, rp.image, rp.local_alias, rp.preferences
|
||||
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
|
||||
@@ -3220,7 +3320,7 @@ getContactRequestChatPreviews_ db User {userId} =
|
||||
[sql|
|
||||
SELECT
|
||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, cr.created_at, cr.updated_at
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
|
||||
FROM contact_requests cr
|
||||
JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id
|
||||
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
|
||||
@@ -3242,13 +3342,13 @@ getContactConnectionChatPreviews_ db User {userId} _ =
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
|
||||
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
|
||||
FROM connections
|
||||
WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_group_link = 0 AND via_contact IS NULL
|
||||
WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_contact IS NULL AND (via_group_link = 0 || (via_group_link = 1 AND group_link_id IS NOT NULL))
|
||||
|]
|
||||
(userId, ConnContact)
|
||||
where
|
||||
toContactConnectionChatPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChat
|
||||
toContactConnectionChatPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChat
|
||||
toContactConnectionChatPreview connRow =
|
||||
let conn = toPendingContactConnection connRow
|
||||
stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False}
|
||||
@@ -3260,7 +3360,7 @@ getPendingContactConnection db userId connId = do
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
|
||||
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
|
||||
FROM connections
|
||||
WHERE user_id = ?
|
||||
AND connection_id = ?
|
||||
@@ -3294,9 +3394,9 @@ updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO ()
|
||||
updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs} =
|
||||
DB.execute db "UPDATE groups SET enable_ntfs = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, userId, groupId)
|
||||
|
||||
toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> PendingContactConnection
|
||||
toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) =
|
||||
PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt}
|
||||
toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> PendingContactConnection
|
||||
toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) =
|
||||
PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt}
|
||||
|
||||
getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect)
|
||||
getDirectChat db user contactId pagination search_ = do
|
||||
@@ -3414,7 +3514,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, cp.local_alias, ct.contact_used, 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.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, 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.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
|
||||
@@ -3522,11 +3622,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.host_conn_custom_user_profile_id, 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, gp.preferences, 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.local_alias
|
||||
pu.display_name, pu.full_name, pu.image, pu.local_alias, pu.preferences
|
||||
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
|
||||
@@ -3536,7 +3636,7 @@ getGroupInfo db User {userId, userContactId} groupId =
|
||||
(groupId, userId, userContactId)
|
||||
|
||||
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, image}
|
||||
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, groupPreferences}} p'@GroupProfile {displayName = newName, fullName, image}
|
||||
| displayName == newName = liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
updateGroupProfile_ currentTs $> (g :: GroupInfo) {groupProfile = p'}
|
||||
@@ -3552,14 +3652,14 @@ updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, grou
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_profiles
|
||||
SET display_name = ?, full_name = ?, image = ?, updated_at = ?
|
||||
SET display_name = ?, full_name = ?, image = ?, preferences = ?, updated_at = ?
|
||||
WHERE group_profile_id IN (
|
||||
SELECT group_profile_id
|
||||
FROM groups
|
||||
WHERE user_id = ? AND group_id = ?
|
||||
)
|
||||
|]
|
||||
(newName, fullName, image, currentTs, userId, groupId)
|
||||
(newName, fullName, image, groupPreferences, currentTs, userId, groupId)
|
||||
updateGroup_ ldn currentTs = do
|
||||
DB.execute
|
||||
db
|
||||
@@ -3865,13 +3965,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.local_alias,
|
||||
p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
||||
-- 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.local_alias
|
||||
rp.display_name, rp.full_name, rp.image, rp.local_alias, rp.preferences
|
||||
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
|
||||
@@ -4484,6 +4584,7 @@ data StoreError
|
||||
| SEProfileNotFound {profileId :: Int64}
|
||||
| SEDuplicateGroupLink {groupInfo :: GroupInfo}
|
||||
| SEGroupLinkNotFound {groupInfo :: GroupInfo}
|
||||
| SEHostMemberIdNotFound {groupId :: Int64}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON StoreError where
|
||||
|
||||
@@ -16,8 +16,8 @@ import Simplex.Chat
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Styled
|
||||
import Simplex.Chat.Terminal.Output
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Chat.View
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8)
|
||||
import System.Exit (exitSuccess)
|
||||
import System.Terminal hiding (insertChars)
|
||||
import UnliftIO.STM
|
||||
@@ -40,7 +40,8 @@ runInputLoop ct cc = forever $ do
|
||||
CRChatCmdError _ -> when (isMessage cmd) $ echo s
|
||||
_ -> pure ()
|
||||
let testV = testView $ config cc
|
||||
printToTerminal ct $ responseToView testV r
|
||||
user <- readTVarIO $ currentUser cc
|
||||
printToTerminal ct $ responseToView user testV r
|
||||
where
|
||||
echo s = printToTerminal ct [plain s]
|
||||
isMessage = \case
|
||||
|
||||
@@ -75,8 +75,10 @@ withTermLock ChatTerminal {termLock} action = do
|
||||
runTerminalOutput :: ChatTerminal -> ChatController -> IO ()
|
||||
runTerminalOutput ct cc = do
|
||||
let testV = testView $ config cc
|
||||
forever $
|
||||
atomically (readTBQueue $ outputQ cc) >>= printToTerminal ct . responseToView testV . snd
|
||||
forever $ do
|
||||
(_, r) <- atomically . readTBQueue $ outputQ cc
|
||||
user <- readTVarIO $ currentUser cc
|
||||
printToTerminal ct $ responseToView user testV r
|
||||
|
||||
printToTerminal :: ChatTerminal -> [StyledString] -> IO ()
|
||||
printToTerminal ct s =
|
||||
|
||||
+333
-11
@@ -19,6 +19,7 @@
|
||||
|
||||
module Simplex.Chat.Types where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
@@ -26,10 +27,12 @@ import qualified Data.Aeson.Types as JT
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString, pack, unpack)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Typeable
|
||||
import Database.SQLite.Simple (ResultError (..), SQLData (..))
|
||||
@@ -40,8 +43,8 @@ import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
|
||||
|
||||
class IsContact a where
|
||||
contactId' :: a -> ContactId
|
||||
@@ -83,6 +86,7 @@ data Contact = Contact
|
||||
viaGroup :: Maybe Int64,
|
||||
contactUsed :: Bool,
|
||||
chatSettings :: ChatSettings,
|
||||
userPreferences :: Preferences,
|
||||
createdAt :: UTCTime,
|
||||
updatedAt :: UTCTime
|
||||
}
|
||||
@@ -99,7 +103,10 @@ contactConnId :: Contact -> ConnId
|
||||
contactConnId Contact {activeConn} = aConnId activeConn
|
||||
|
||||
contactConnIncognito :: Contact -> Bool
|
||||
contactConnIncognito Contact {activeConn = Connection {customUserProfileId}} = isJust customUserProfileId
|
||||
contactConnIncognito = isJust . customUserProfileId'
|
||||
|
||||
customUserProfileId' :: Contact -> Maybe Int64
|
||||
customUserProfileId' Contact {activeConn} = customUserProfileId (activeConn :: Connection)
|
||||
|
||||
data ContactRef = ContactRef
|
||||
{ contactId :: ContactId,
|
||||
@@ -227,10 +234,277 @@ defaultChatSettings = ChatSettings {enableNtfs = True}
|
||||
pattern DisableNtfs :: ChatSettings
|
||||
pattern DisableNtfs = ChatSettings {enableNtfs = False}
|
||||
|
||||
data ChatFeature
|
||||
= CFFullDelete
|
||||
| -- | CFReceipts
|
||||
CFVoice
|
||||
|
||||
allChatFeatures :: [ChatFeature]
|
||||
allChatFeatures =
|
||||
[ CFFullDelete,
|
||||
-- CFReceipts,
|
||||
CFVoice
|
||||
]
|
||||
|
||||
chatPrefSel :: ChatFeature -> Preferences -> Maybe Preference
|
||||
chatPrefSel = \case
|
||||
CFFullDelete -> fullDelete
|
||||
-- CFReceipts -> receipts
|
||||
CFVoice -> voice
|
||||
|
||||
chatPrefName :: ChatFeature -> Text
|
||||
chatPrefName = \case
|
||||
CFFullDelete -> "full message deletion"
|
||||
-- CFReceipts -> "delivery receipts"
|
||||
CFVoice -> "voice messages"
|
||||
|
||||
class HasPreferences p where
|
||||
preferences' :: p -> Maybe Preferences
|
||||
|
||||
instance HasPreferences User where
|
||||
preferences' User {profile = LocalProfile {preferences}} = preferences
|
||||
{-# INLINE preferences' #-}
|
||||
|
||||
instance HasPreferences Contact where
|
||||
preferences' Contact {profile = LocalProfile {preferences}} = preferences
|
||||
{-# INLINE preferences' #-}
|
||||
|
||||
class PreferenceI p where
|
||||
getPreference :: ChatFeature -> p -> Preference
|
||||
|
||||
instance PreferenceI Preferences where
|
||||
getPreference pt prefs = fromMaybe (getPreference pt defaultChatPrefs) (chatPrefSel pt prefs)
|
||||
|
||||
instance PreferenceI (Maybe Preferences) where
|
||||
getPreference pt prefs = fromMaybe (getPreference pt defaultChatPrefs) (chatPrefSel pt =<< prefs)
|
||||
|
||||
instance PreferenceI FullPreferences where
|
||||
getPreference = \case
|
||||
CFFullDelete -> fullDelete
|
||||
-- CFReceipts -> receipts
|
||||
CFVoice -> voice
|
||||
{-# INLINE getPreference #-}
|
||||
|
||||
-- collection of optional chat preferences for the user and the contact
|
||||
data Preferences = Preferences
|
||||
{ fullDelete :: Maybe Preference,
|
||||
-- receipts :: Maybe Preference,
|
||||
voice :: Maybe Preference
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
data GroupPreferences = GroupPreferences
|
||||
{ fullDelete :: Maybe GroupPreference,
|
||||
-- receipts :: Maybe GroupPreference,
|
||||
voice :: Maybe GroupPreference
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON GroupPreferences where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
instance ToField GroupPreferences where
|
||||
toField = toField . encodeJSON
|
||||
|
||||
instance FromField GroupPreferences where
|
||||
fromField = fromTextField_ decodeJSON
|
||||
|
||||
-- full collection of chat preferences defined in the app - it is used to ensure we include all preferences and to simplify processing
|
||||
-- if some of the preferences are not defined in Preferences, defaults from defaultChatPrefs are used here.
|
||||
data FullPreferences = FullPreferences
|
||||
{ fullDelete :: Preference,
|
||||
-- receipts :: Preference,
|
||||
voice :: Preference
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
-- merged preferences of user for a given contact - they differentiate between specific preferences for the contact and global user preferences
|
||||
data ContactUserPreferences = ContactUserPreferences
|
||||
{ fullDelete :: ContactUserPreference,
|
||||
-- receipts :: ContactUserPreference,
|
||||
voice :: ContactUserPreference
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
data ContactUserPreference = ContactUserPreference
|
||||
{ enabled :: PrefEnabled,
|
||||
userPreference :: ContactUserPref,
|
||||
contactPreference :: Preference
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
data ContactUserPref = CUPContact {preference :: Preference} | CUPUser {preference :: Preference}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON ContactUserPreferences where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
instance ToJSON ContactUserPreference where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
instance ToJSON ContactUserPref where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CUP"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CUP"
|
||||
|
||||
toChatPrefs :: FullPreferences -> Preferences
|
||||
toChatPrefs FullPreferences {fullDelete, voice} =
|
||||
Preferences
|
||||
{ fullDelete = Just fullDelete,
|
||||
-- receipts = Just receipts,
|
||||
voice = Just voice
|
||||
}
|
||||
|
||||
defaultChatPrefs :: FullPreferences
|
||||
defaultChatPrefs =
|
||||
FullPreferences
|
||||
{ fullDelete = Preference {allow = FANo},
|
||||
-- receipts = Preference {allow = FANo},
|
||||
voice = Preference {allow = FAYes}
|
||||
}
|
||||
|
||||
emptyChatPrefs :: Preferences
|
||||
emptyChatPrefs = Preferences Nothing Nothing
|
||||
|
||||
instance ToJSON Preferences where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
instance ToField Preferences where
|
||||
toField = toField . encodeJSON
|
||||
|
||||
instance FromField Preferences where
|
||||
fromField = fromTextField_ decodeJSON
|
||||
|
||||
data Preference = Preference
|
||||
{allow :: FeatureAllowed}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
data GroupPreference = GroupPreference
|
||||
{enable :: GroupFeatureEnabled}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON Preference where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
instance ToJSON GroupPreference where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data FeatureAllowed
|
||||
= FAAlways -- allow unconditionally
|
||||
| FAYes -- allow, if peer allows it
|
||||
| FANo -- do not allow
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
data GroupFeatureEnabled = FEOn | FEOff
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance FromField FeatureAllowed where fromField = fromBlobField_ strDecode
|
||||
|
||||
instance ToField FeatureAllowed where toField = toField . strEncode
|
||||
|
||||
instance StrEncoding FeatureAllowed where
|
||||
strEncode = \case
|
||||
FAAlways -> "always"
|
||||
FAYes -> "yes"
|
||||
FANo -> "no"
|
||||
strDecode = \case
|
||||
"always" -> Right FAAlways
|
||||
"yes" -> Right FAYes
|
||||
"no" -> Right FANo
|
||||
r -> Left $ "bad FeatureAllowed " <> B.unpack r
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance FromJSON FeatureAllowed where
|
||||
parseJSON = strParseJSON "FeatureAllowed"
|
||||
|
||||
instance ToJSON FeatureAllowed where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromField GroupFeatureEnabled where fromField = fromBlobField_ strDecode
|
||||
|
||||
instance ToField GroupFeatureEnabled where toField = toField . strEncode
|
||||
|
||||
instance StrEncoding GroupFeatureEnabled where
|
||||
strEncode = \case
|
||||
FEOn -> "on"
|
||||
FEOff -> "off"
|
||||
strDecode = \case
|
||||
"on" -> Right FEOn
|
||||
"off" -> Right FEOff
|
||||
r -> Left $ "bad GroupFeatureEnabled " <> B.unpack r
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance FromJSON GroupFeatureEnabled where
|
||||
parseJSON = strParseJSON "GroupFeatureEnabled"
|
||||
|
||||
instance ToJSON GroupFeatureEnabled where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
mergePreferences :: Maybe Preferences -> Maybe Preferences -> FullPreferences
|
||||
mergePreferences contactPrefs userPreferences =
|
||||
FullPreferences
|
||||
{ fullDelete = pref CFFullDelete,
|
||||
-- receipts = pref CFReceipts,
|
||||
voice = pref CFVoice
|
||||
}
|
||||
where
|
||||
pref pt =
|
||||
let sel = chatPrefSel pt
|
||||
in fromMaybe (getPreference pt defaultChatPrefs) $ (contactPrefs >>= sel) <|> (userPreferences >>= sel)
|
||||
|
||||
mergeUserChatPrefs :: User -> Contact -> FullPreferences
|
||||
mergeUserChatPrefs user ct =
|
||||
let userPrefs = if contactConnIncognito ct then Nothing else preferences' user
|
||||
in mergePreferences (Just $ userPreferences ct) userPrefs
|
||||
|
||||
data PrefEnabled = PrefEnabled {forUser :: Bool, forContact :: Bool}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON PrefEnabled where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
prefEnabled :: Preference -> Preference -> PrefEnabled
|
||||
prefEnabled Preference {allow = user} Preference {allow = contact} = case (user, contact) of
|
||||
(FAAlways, FANo) -> PrefEnabled {forUser = False, forContact = True}
|
||||
(FANo, FAAlways) -> PrefEnabled {forUser = True, forContact = False}
|
||||
(_, FANo) -> PrefEnabled False False
|
||||
(FANo, _) -> PrefEnabled False False
|
||||
_ -> PrefEnabled True True
|
||||
|
||||
contactUserPreferences :: User -> Contact -> ContactUserPreferences
|
||||
contactUserPreferences user ct =
|
||||
ContactUserPreferences
|
||||
{ fullDelete = pref CFFullDelete,
|
||||
-- receipts = pref CFReceipts,
|
||||
voice = pref CFVoice
|
||||
}
|
||||
where
|
||||
pref pt =
|
||||
ContactUserPreference
|
||||
{ enabled = prefEnabled userPref ctPref,
|
||||
-- incognito contact cannot have default user preference used
|
||||
userPreference = if contactConnIncognito ct then CUPContact ctUserPref else maybe (CUPUser userPref) CUPContact ctUserPref_,
|
||||
contactPreference = ctPref
|
||||
}
|
||||
where
|
||||
ctUserPref = getPreference pt $ userPreferences ct
|
||||
ctUserPref_ = chatPrefSel pt $ userPreferences ct
|
||||
userPref = getPreference pt ctUserPrefs
|
||||
ctPref = getPreference pt ctPrefs
|
||||
ctUserPrefs = mergeUserChatPrefs user ct
|
||||
ctPrefs = mergePreferences (preferences' ct) Nothing
|
||||
|
||||
getContactUserPrefefence :: ChatFeature -> ContactUserPreferences -> ContactUserPreference
|
||||
getContactUserPrefefence = \case
|
||||
CFFullDelete -> fullDelete
|
||||
-- CFReceipts -> receipts
|
||||
CFVoice -> voice
|
||||
|
||||
data Profile = Profile
|
||||
{ displayName :: ContactName,
|
||||
fullName :: Text,
|
||||
image :: Maybe ImageData
|
||||
image :: Maybe ImageData,
|
||||
preferences :: Maybe Preferences
|
||||
-- fields that should not be read into this data type to prevent sending them as part of profile to contacts:
|
||||
-- - contact_profile_id
|
||||
-- - incognito
|
||||
@@ -251,6 +525,7 @@ data LocalProfile = LocalProfile
|
||||
displayName :: ContactName,
|
||||
fullName :: Text,
|
||||
image :: Maybe ImageData,
|
||||
preferences :: Maybe Preferences,
|
||||
localAlias :: LocalAlias
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
@@ -263,17 +538,18 @@ localProfileId :: LocalProfile -> ProfileId
|
||||
localProfileId = profileId
|
||||
|
||||
toLocalProfile :: ProfileId -> Profile -> LocalAlias -> LocalProfile
|
||||
toLocalProfile profileId Profile {displayName, fullName, image} localAlias =
|
||||
LocalProfile {profileId, displayName, fullName, image, localAlias}
|
||||
toLocalProfile profileId Profile {displayName, fullName, image, preferences} localAlias =
|
||||
LocalProfile {profileId, displayName, fullName, image, preferences, localAlias}
|
||||
|
||||
fromLocalProfile :: LocalProfile -> Profile
|
||||
fromLocalProfile LocalProfile {displayName, fullName, image} =
|
||||
Profile {displayName, fullName, image}
|
||||
fromLocalProfile LocalProfile {displayName, fullName, image, preferences} =
|
||||
Profile {displayName, fullName, image, preferences}
|
||||
|
||||
data GroupProfile = GroupProfile
|
||||
{ displayName :: GroupName,
|
||||
fullName :: Text,
|
||||
image :: Maybe ImageData
|
||||
image :: Maybe ImageData,
|
||||
groupPreferences :: Maybe GroupPreferences
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
@@ -295,11 +571,41 @@ instance ToField ImageData where toField (ImageData t) = toField t
|
||||
|
||||
instance FromField ImageData where fromField = fmap ImageData . fromField
|
||||
|
||||
data CReqClientData = CRDataGroup {groupLinkId :: GroupLinkId}
|
||||
deriving (Generic)
|
||||
|
||||
instance ToJSON CReqClientData where
|
||||
toJSON = J.genericToJSON . taggedObjectJSON $ dropPrefix "CRData"
|
||||
toEncoding = J.genericToEncoding . taggedObjectJSON $ dropPrefix "CRData"
|
||||
|
||||
instance FromJSON CReqClientData where
|
||||
parseJSON = J.genericParseJSON . taggedObjectJSON $ dropPrefix "CRData"
|
||||
|
||||
newtype GroupLinkId = GroupLinkId {unGroupLinkId :: ByteString} -- used to identify invitation via group link
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance FromField GroupLinkId where fromField f = GroupLinkId <$> fromField f
|
||||
|
||||
instance ToField GroupLinkId where toField (GroupLinkId g) = toField g
|
||||
|
||||
instance StrEncoding GroupLinkId where
|
||||
strEncode (GroupLinkId g) = strEncode g
|
||||
strDecode s = GroupLinkId <$> strDecode s
|
||||
strP = GroupLinkId <$> strP
|
||||
|
||||
instance FromJSON GroupLinkId where
|
||||
parseJSON = strParseJSON "GroupLinkId"
|
||||
|
||||
instance ToJSON GroupLinkId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
data GroupInvitation = GroupInvitation
|
||||
{ fromMember :: MemberIdRole,
|
||||
invitedMember :: MemberIdRole,
|
||||
connRequest :: ConnReqInvitation,
|
||||
groupProfile :: GroupProfile
|
||||
groupProfile :: GroupProfile,
|
||||
groupLinkId :: Maybe GroupLinkId
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
@@ -367,6 +673,15 @@ instance ToJSON GroupMember where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
data GroupMemberRef = GroupMemberRef {groupMemberId :: Int64, profile :: Profile}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON GroupMemberRef where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
groupMemberRef :: GroupMember -> GroupMemberRef
|
||||
groupMemberRef GroupMember {groupMemberId, memberProfile = p} =
|
||||
GroupMemberRef {groupMemberId, profile = fromLocalProfile p}
|
||||
|
||||
memberConn :: GroupMember -> Maybe Connection
|
||||
memberConn = activeConn
|
||||
|
||||
@@ -831,6 +1146,7 @@ data PendingContactConnection = PendingContactConnection
|
||||
pccConnStatus :: ConnStatus,
|
||||
viaContactUri :: Bool,
|
||||
viaUserContactLink :: Maybe Int64,
|
||||
groupLinkId :: Maybe GroupLinkId,
|
||||
customUserProfileId :: Maybe Int64,
|
||||
connReqInv :: Maybe ConnReqInvitation,
|
||||
localAlias :: Text,
|
||||
@@ -1059,3 +1375,9 @@ data XGrpMemIntroCont = XGrpMemIntroCont
|
||||
groupConnReq :: ConnReqInvitation
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
encodeJSON :: ToJSON a => a -> Text
|
||||
encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode
|
||||
|
||||
decodeJSON :: FromJSON a => Text -> Maybe a
|
||||
decodeJSON = J.decode . LB.fromStrict . encodeUtf8
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
module Simplex.Chat.Util where
|
||||
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8With)
|
||||
|
||||
safeDecodeUtf8 :: ByteString -> Text
|
||||
safeDecodeUtf8 = decodeUtf8With onError
|
||||
where
|
||||
onError _ _ = Just '?'
|
||||
+100
-24
@@ -18,7 +18,7 @@ import Data.Char (toUpper)
|
||||
import Data.Function (on)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
|
||||
import Data.Maybe (isJust, isNothing)
|
||||
import Data.Maybe (isJust, isNothing, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (DiffTime)
|
||||
@@ -49,11 +49,11 @@ import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import System.Console.ANSI.Types
|
||||
|
||||
serializeChatResponse :: ChatResponse -> String
|
||||
serializeChatResponse = unlines . map unStyle . responseToView False
|
||||
serializeChatResponse :: Maybe User -> ChatResponse -> String
|
||||
serializeChatResponse user_ = unlines . map unStyle . responseToView user_ False
|
||||
|
||||
responseToView :: Bool -> ChatResponse -> [StyledString]
|
||||
responseToView testView = \case
|
||||
responseToView :: Maybe User -> Bool -> ChatResponse -> [StyledString]
|
||||
responseToView user_ testView = \case
|
||||
CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile
|
||||
CRChatStarted -> ["chat started"]
|
||||
CRChatRunning -> ["chat is running"]
|
||||
@@ -67,6 +67,8 @@ responseToView testView = \case
|
||||
CRNetworkConfig cfg -> viewNetworkConfig cfg
|
||||
CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile
|
||||
CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats
|
||||
CRContactSwitch ct progress -> viewContactSwitch ct progress
|
||||
CRGroupMemberSwitch g m progress -> viewGroupMemberSwitch g m progress
|
||||
CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False
|
||||
CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True) chatItems
|
||||
CRChatItemStatusUpdated _ -> []
|
||||
@@ -112,7 +114,7 @@ responseToView testView = \case
|
||||
CRContactRequestAlreadyAccepted c -> [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"]
|
||||
CRUserContactLinkCreated cReq -> connReqContact_ "Your new chat address is created!" cReq
|
||||
CRUserContactLinkDeleted -> viewUserContactLinkDeleted
|
||||
CRUserAcceptedGroupSent _g -> [] -- [ttyGroup' g <> ": joining the group..."]
|
||||
CRUserAcceptedGroupSent _g _ -> [] -- [ttyGroup' g <> ": joining the group..."]
|
||||
CRUserDeletedMember g m -> [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"]
|
||||
CRLeftMemberUser g -> [ttyGroup' g <> ": you left the group"] <> groupPreserved g
|
||||
CRGroupDeletedUser g -> [ttyGroup' g <> ": you deleted the group"]
|
||||
@@ -121,9 +123,14 @@ responseToView testView = \case
|
||||
CRSndGroupFileCancelled _ ftm fts -> viewSndGroupFileCancelled ftm fts
|
||||
CRRcvFileCancelled ft -> receivingFile_ "cancelled" ft
|
||||
CRUserProfileUpdated p p' -> viewUserProfileUpdated p p'
|
||||
CRContactPrefsUpdated {fromContact, toContact, preferences} -> case user_ of
|
||||
Just user -> viewUserContactPrefsUpdated user fromContact toContact preferences
|
||||
_ -> ["unexpected chat event CRContactPrefsUpdated without current user"]
|
||||
CRContactAliasUpdated c -> viewContactAliasUpdated c
|
||||
CRConnectionAliasUpdated c -> viewConnectionAliasUpdated c
|
||||
CRContactUpdated c c' -> viewContactUpdated c c'
|
||||
CRContactUpdated {fromContact = c, toContact = c', preferences} -> case user_ of
|
||||
Just user -> viewContactUpdated c c' <> viewContactPrefsUpdated user c c' preferences
|
||||
_ -> ["unexpected chat event CRContactUpdated without current user"]
|
||||
CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt
|
||||
CRReceivedContactRequest UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile
|
||||
CRRcvFileStart ci -> receivingFile_' "started" ci
|
||||
@@ -254,19 +261,15 @@ viewChatItem chat ChatItem {chatDir, meta, content, quotedItem, file} doShow = c
|
||||
DirectChat c -> case chatDir of
|
||||
CIDirectSnd -> case content of
|
||||
CISndMsgContent mc -> withSndFile to $ sndMsg to quote mc
|
||||
CISndDeleted _ -> showSndItem to
|
||||
CISndCall {} -> showSndItem to
|
||||
CISndGroupInvitation {} -> showSndItem to
|
||||
CISndGroupEvent {} -> showSndItemProhibited to
|
||||
_ -> showSndItem to
|
||||
where
|
||||
to = ttyToContact' c
|
||||
CIDirectRcv -> case content of
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
|
||||
CIRcvDeleted _ -> showRcvItem from
|
||||
CIRcvCall {} -> showRcvItem from
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err meta
|
||||
CIRcvGroupInvitation {} -> showRcvItem from
|
||||
CIRcvGroupEvent {} -> showRcvItemProhibited from
|
||||
_ -> showRcvItem from
|
||||
where
|
||||
from = ttyFromContact' c
|
||||
where
|
||||
@@ -274,19 +277,15 @@ viewChatItem chat ChatItem {chatDir, meta, content, quotedItem, file} doShow = c
|
||||
GroupChat g -> case chatDir of
|
||||
CIGroupSnd -> case content of
|
||||
CISndMsgContent mc -> withSndFile to $ sndMsg to quote mc
|
||||
CISndDeleted _ -> showSndItem to
|
||||
CISndCall {} -> showSndItem to
|
||||
CISndGroupInvitation {} -> showSndItemProhibited to
|
||||
CISndGroupEvent {} -> showSndItem to
|
||||
_ -> showSndItem to
|
||||
where
|
||||
to = ttyToGroup g
|
||||
CIGroupRcv m -> case content of
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
|
||||
CIRcvDeleted _ -> showRcvItem from
|
||||
CIRcvCall {} -> showRcvItem from
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err meta
|
||||
CIRcvGroupInvitation {} -> showRcvItemProhibited from
|
||||
CIRcvGroupEvent {} -> showRcvItem from
|
||||
_ -> showRcvItem from
|
||||
where
|
||||
from = ttyFromGroup' g m
|
||||
where
|
||||
@@ -682,15 +681,92 @@ viewServers = plain . intercalate ", " . map (B.unpack . strEncode)
|
||||
viewServerHosts :: [SMPServer] -> StyledString
|
||||
viewServerHosts = plain . intercalate ", " . map showSMPServer
|
||||
|
||||
viewContactSwitch :: Contact -> SwitchProgress -> [StyledString]
|
||||
viewContactSwitch _ (SwitchProgress _ SPConfirmed _) = []
|
||||
viewContactSwitch ct (SwitchProgress qd phase _) = case qd of
|
||||
QDRcv -> [ttyContact' ct <> ": you " <> viewSwitchPhase phase]
|
||||
QDSnd -> [ttyContact' ct <> " " <> viewSwitchPhase phase <> " for you"]
|
||||
|
||||
viewGroupMemberSwitch :: GroupInfo -> GroupMember -> SwitchProgress -> [StyledString]
|
||||
viewGroupMemberSwitch _ _ (SwitchProgress _ SPConfirmed _) = []
|
||||
viewGroupMemberSwitch g m (SwitchProgress qd phase _) = case qd of
|
||||
QDRcv -> [ttyGroup' g <> ": you " <> viewSwitchPhase phase <> " for " <> ttyMember m]
|
||||
QDSnd -> [ttyGroup' g <> ": " <> ttyMember m <> " " <> viewSwitchPhase phase <> " for you"]
|
||||
|
||||
viewSwitchPhase :: SwitchPhase -> StyledString
|
||||
viewSwitchPhase SPCompleted = "changed address"
|
||||
viewSwitchPhase phase = plain (strEncode phase) <> " changing address"
|
||||
|
||||
viewUserProfileUpdated :: Profile -> Profile -> [StyledString]
|
||||
viewUserProfileUpdated Profile {displayName = n, fullName, image} Profile {displayName = n', fullName = fullName', image = image'}
|
||||
| n == n' && fullName == fullName' && image == image' = []
|
||||
| n == n' && fullName == fullName' = [if isNothing image' then "profile image removed" else "profile image updated"]
|
||||
| n == n' = ["user full name " <> (if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName') <> notified]
|
||||
| otherwise = ["user profile is changed to " <> ttyFullName n' fullName' <> notified]
|
||||
viewUserProfileUpdated Profile {displayName = n, fullName, image, preferences} Profile {displayName = n', fullName = fullName', image = image', preferences = prefs'} =
|
||||
profileUpdated <> viewPrefsUpdated preferences prefs'
|
||||
where
|
||||
profileUpdated
|
||||
| n == n' && fullName == fullName' && image == image' = []
|
||||
| n == n' && fullName == fullName' = [if isNothing image' then "profile image removed" else "profile image updated"]
|
||||
| n == n' = ["user full name " <> (if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName') <> notified]
|
||||
| otherwise = ["user profile is changed to " <> ttyFullName n' fullName' <> notified]
|
||||
notified = " (your contacts are notified)"
|
||||
|
||||
viewUserContactPrefsUpdated :: User -> Contact -> Contact -> ContactUserPreferences -> [StyledString]
|
||||
viewUserContactPrefsUpdated user ct ct' cups
|
||||
| null prefs = ["your preferences for " <> ttyContact' ct' <> " did not change"]
|
||||
| otherwise = ("you updated preferences for " <> ttyContact' ct' <> ":") : prefs
|
||||
where
|
||||
prefs = viewContactPreferences user ct ct' cups
|
||||
|
||||
viewContactPrefsUpdated :: User -> Contact -> Contact -> ContactUserPreferences -> [StyledString]
|
||||
viewContactPrefsUpdated user ct ct' cups
|
||||
| null prefs = []
|
||||
| otherwise = (ttyContact' ct' <> " updated preferences for you:") : prefs
|
||||
where
|
||||
prefs = viewContactPreferences user ct ct' cups
|
||||
|
||||
viewContactPreferences :: User -> Contact -> Contact -> ContactUserPreferences -> [StyledString]
|
||||
viewContactPreferences user ct ct' cups =
|
||||
mapMaybe (viewContactPref (mergeUserChatPrefs user ct) (mergeUserChatPrefs user ct') (preferences' ct) cups) allChatFeatures
|
||||
|
||||
viewContactPref :: FullPreferences -> FullPreferences -> Maybe Preferences -> ContactUserPreferences -> ChatFeature -> Maybe StyledString
|
||||
viewContactPref userPrefs userPrefs' ctPrefs cups pt
|
||||
| userPref == userPref' && ctPref == contactPreference = Nothing
|
||||
| otherwise = Just $ plain (chatPrefName pt) <> ": " <> viewPrefEnabled enabled <> " (you allow: " <> viewCountactUserPref userPreference <> ", contact allows: " <> viewPreference contactPreference <> ")"
|
||||
where
|
||||
userPref = getPreference pt userPrefs
|
||||
userPref' = getPreference pt userPrefs'
|
||||
ctPref = getPreference pt ctPrefs
|
||||
ContactUserPreference {enabled, userPreference, contactPreference} = getContactUserPrefefence pt cups
|
||||
|
||||
viewPrefsUpdated :: Maybe Preferences -> Maybe Preferences -> [StyledString]
|
||||
viewPrefsUpdated ps ps'
|
||||
| null prefs = []
|
||||
| otherwise = "updated preferences:" : prefs
|
||||
where
|
||||
prefs = mapMaybe viewPref allChatFeatures
|
||||
viewPref pt
|
||||
| pref ps == pref ps' = Nothing
|
||||
| otherwise = Just $ plain (chatPrefName pt) <> " allowed: " <> viewPreference (pref ps')
|
||||
where
|
||||
pref pss = getPreference pt $ mergePreferences pss Nothing
|
||||
|
||||
viewPreference :: Preference -> StyledString
|
||||
viewPreference = \case
|
||||
Preference {allow} -> case allow of
|
||||
FAAlways -> "always"
|
||||
FAYes -> "yes"
|
||||
FANo -> "no"
|
||||
|
||||
viewCountactUserPref :: ContactUserPref -> StyledString
|
||||
viewCountactUserPref = \case
|
||||
CUPUser p -> "default (" <> viewPreference p <> ")"
|
||||
CUPContact p -> viewPreference p
|
||||
|
||||
viewPrefEnabled :: PrefEnabled -> StyledString
|
||||
viewPrefEnabled = \case
|
||||
PrefEnabled True True -> "enabled"
|
||||
PrefEnabled False False -> "off"
|
||||
PrefEnabled {forUser = True, forContact = False} -> "enabled for you"
|
||||
PrefEnabled {forUser = False, forContact = True} -> "enabled for contact"
|
||||
|
||||
viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> [StyledString]
|
||||
viewGroupUpdated
|
||||
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, image}}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 19aef52135b288dc92c4a2ec6d0be4b8283649ab
|
||||
commit: 029fc6e781d78249d0b3a17edff7416ef22afed1
|
||||
# - ../direct-sqlcipher
|
||||
- github: simplex-chat/direct-sqlcipher
|
||||
commit: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||
|
||||
+226
-115
@@ -24,24 +24,27 @@ import qualified Data.Text as T
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig)
|
||||
import Simplex.Chat.Options (ChatOpts (..))
|
||||
import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), ImageData (..), LocalProfile (..), Profile (..), User (..))
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Util (unlessM)
|
||||
import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec
|
||||
|
||||
defaultPrefs :: Maybe Preferences
|
||||
defaultPrefs = Just $ toChatPrefs defaultChatPrefs
|
||||
|
||||
aliceProfile :: Profile
|
||||
aliceProfile = Profile {displayName = "alice", fullName = "Alice", image = Nothing}
|
||||
aliceProfile = Profile {displayName = "alice", fullName = "Alice", image = Nothing, preferences = defaultPrefs}
|
||||
|
||||
bobProfile :: Profile
|
||||
bobProfile = Profile {displayName = "bob", fullName = "Bob", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC")}
|
||||
bobProfile = Profile {displayName = "bob", fullName = "Bob", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC"), preferences = defaultPrefs}
|
||||
|
||||
cathProfile :: Profile
|
||||
cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Nothing}
|
||||
cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Nothing, preferences = defaultPrefs}
|
||||
|
||||
danProfile :: Profile
|
||||
danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing}
|
||||
danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, preferences = defaultPrefs}
|
||||
|
||||
chatTests :: Spec
|
||||
chatTests = do
|
||||
@@ -107,9 +110,11 @@ chatTests = do
|
||||
it "accept contact request incognito" testAcceptContactRequestIncognito
|
||||
it "join group incognito" testJoinGroupIncognito
|
||||
it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito
|
||||
describe "contact aliases" $ do
|
||||
it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito
|
||||
describe "contact aliases and prefs" $ do
|
||||
it "set contact alias" testSetAlias
|
||||
it "set connection alias" testSetConnectionAlias
|
||||
it "set contact prefs" testSetContactPrefs
|
||||
describe "SMP servers" $
|
||||
it "get and set SMP servers" testGetSetSMPServers
|
||||
describe "async connection handshake" $ do
|
||||
@@ -138,8 +143,11 @@ chatTests = do
|
||||
it "set chat item TTL" testSetChatItemTTL
|
||||
describe "group links" $ do
|
||||
it "create group link, join via group link" testGroupLink
|
||||
it "sending message to contact created via group link marks it used" testGroupLinkContactUsed
|
||||
it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership
|
||||
it "deleting invited member does not leave broken chat item" testGroupLinkDeleteInvitedMemberNoBrokenItem
|
||||
describe "queue rotation" $ do
|
||||
it "switch contact to a different queue" testSwitchContact
|
||||
it "switch group member to a different queue" testSwitchGroupMember
|
||||
|
||||
versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec
|
||||
versionTestMatrix2 runTest = do
|
||||
@@ -768,7 +776,13 @@ testGroupDelete =
|
||||
cath <## "#team: you deleted the group"
|
||||
alice <##> bob
|
||||
alice <##> cath
|
||||
bob <##> cath
|
||||
-- unused group contacts are deleted
|
||||
bob ##> "@cath hi"
|
||||
bob <## "no contact cath"
|
||||
(cath </)
|
||||
cath ##> "@bob hi"
|
||||
cath <## "no contact bob"
|
||||
(bob </)
|
||||
|
||||
testGroupSameName :: IO ()
|
||||
testGroupSameName =
|
||||
@@ -2398,6 +2412,22 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi
|
||||
bob ?<# (aliceIncognito <> "> do you see that I've changed profile?")
|
||||
bob ?#> ("@" <> aliceIncognito <> " no")
|
||||
alice ?<# (bobIncognito <> "> no")
|
||||
alice ##> "/_set prefs @2 {}"
|
||||
alice <## ("your preferences for " <> bobIncognito <> " did not change")
|
||||
(bob </)
|
||||
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}"
|
||||
alice <## ("you updated preferences for " <> bobIncognito <> ":")
|
||||
alice <## "full message deletion: enabled for contact (you allow: always, contact allows: no)"
|
||||
bob <## (aliceIncognito <> " updated preferences for you:")
|
||||
bob <## "full message deletion: enabled for you (you allow: no, contact allows: always)"
|
||||
bob ##> "/_set prefs @2 {}"
|
||||
bob <## ("your preferences for " <> aliceIncognito <> " did not change")
|
||||
(alice </)
|
||||
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"no\"}}"
|
||||
alice <## ("you updated preferences for " <> bobIncognito <> ":")
|
||||
alice <## "full message deletion: off (you allow: no, contact allows: no)"
|
||||
bob <## (aliceIncognito <> " updated preferences for you:")
|
||||
bob <## "full message deletion: off (you allow: no, contact allows: no)"
|
||||
|
||||
testConnectIncognitoContactAddress :: IO ()
|
||||
testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
|
||||
@@ -2670,6 +2700,59 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
|
||||
-- bob doesn't receive invitation
|
||||
(bob </)
|
||||
|
||||
testCantSeeGlobalPrefsUpdateIncognito :: IO ()
|
||||
testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
alice #$> ("/incognito on", id, "ok")
|
||||
alice ##> "/c"
|
||||
invIncognito <- getInvitation alice
|
||||
alice #$> ("/incognito off", id, "ok")
|
||||
alice ##> "/c"
|
||||
inv <- getInvitation alice
|
||||
bob ##> ("/c " <> invIncognito)
|
||||
bob <## "confirmation sent!"
|
||||
aliceIncognito <- getTermLine alice
|
||||
cath ##> ("/c " <> inv)
|
||||
cath <## "confirmation sent!"
|
||||
concurrentlyN_
|
||||
[ bob <## (aliceIncognito <> ": contact is connected"),
|
||||
do
|
||||
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
|
||||
alice <## "use /info bob to print out this incognito profile again",
|
||||
do
|
||||
cath <## "alice (Alice): contact is connected"
|
||||
]
|
||||
alice <## "cath (Catherine): contact is connected"
|
||||
alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}}}"
|
||||
alice <## "user full name removed (your contacts are notified)"
|
||||
alice <## "updated preferences:"
|
||||
alice <## "full message deletion allowed: always"
|
||||
(alice </)
|
||||
-- bob doesn't receive profile update
|
||||
(bob </)
|
||||
cath <## "contact alice removed full name"
|
||||
cath <## "alice updated preferences for you:"
|
||||
cath <## "full message deletion: enabled for you (you allow: default (no), contact allows: always)"
|
||||
(cath </)
|
||||
bob ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}"
|
||||
bob <## ("you updated preferences for " <> aliceIncognito <> ":")
|
||||
bob <## "full message deletion: enabled for contact (you allow: always, contact allows: no)"
|
||||
alice <## "bob updated preferences for you:"
|
||||
alice <## "full message deletion: enabled for you (you allow: no, contact allows: always)"
|
||||
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"yes\"}}"
|
||||
alice <## "you updated preferences for bob:"
|
||||
alice <## "full message deletion: enabled (you allow: yes, contact allows: always)"
|
||||
bob <## (aliceIncognito <> " updated preferences for you:")
|
||||
bob <## "full message deletion: enabled (you allow: always, contact allows: yes)"
|
||||
(cath </)
|
||||
alice ##> "/_set prefs @3 {\"fullDelete\": {\"allow\": \"always\"}}"
|
||||
alice <## "your preferences for cath did not change"
|
||||
alice ##> "/_set prefs @3 {\"fullDelete\": {\"allow\": \"yes\"}}"
|
||||
alice <## "you updated preferences for cath:"
|
||||
alice <## "full message deletion: off (you allow: yes, contact allows: no)"
|
||||
cath <## "alice updated preferences for you:"
|
||||
cath <## "full message deletion: off (you allow: default (no), contact allows: yes)"
|
||||
|
||||
testSetAlias :: IO ()
|
||||
testSetAlias = testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
@@ -2698,6 +2781,45 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $
|
||||
alice ##> "/cs"
|
||||
alice <## "bob (Bob) (alias: friend)"
|
||||
|
||||
testSetContactPrefs :: IO ()
|
||||
testSetContactPrefs = testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
alice ##> "/_set prefs @2 {}"
|
||||
alice <## "your preferences for bob did not change"
|
||||
(bob </)
|
||||
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"always\"}}"
|
||||
alice <## "you updated preferences for bob:"
|
||||
alice <## "full message deletion: enabled for contact (you allow: always, contact allows: no)"
|
||||
bob <## "alice updated preferences for you:"
|
||||
bob <## "full message deletion: enabled for you (you allow: default (no), contact allows: always)"
|
||||
(bob </)
|
||||
alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"no\"}}}"
|
||||
alice <## "user full name removed (your contacts are notified)"
|
||||
bob <## "contact alice removed full name"
|
||||
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"yes\"}}"
|
||||
alice <## "you updated preferences for bob:"
|
||||
alice <## "full message deletion: off (you allow: yes, contact allows: no)"
|
||||
bob <## "alice updated preferences for you:"
|
||||
bob <## "full message deletion: off (you allow: default (no), contact allows: yes)"
|
||||
(bob </)
|
||||
bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"yes\"}}}"
|
||||
bob <## "user full name removed (your contacts are notified)"
|
||||
bob <## "updated preferences:"
|
||||
bob <## "full message deletion allowed: yes"
|
||||
alice <## "contact bob removed full name"
|
||||
alice <## "bob updated preferences for you:"
|
||||
alice <## "full message deletion: enabled (you allow: yes, contact allows: yes)"
|
||||
(alice </)
|
||||
bob ##> "/_set prefs @2 {}"
|
||||
bob <## "your preferences for alice did not change"
|
||||
(alice </)
|
||||
alice ##> "/_set prefs @2 {\"fullDelete\": {\"allow\": \"no\"}}"
|
||||
alice <## "you updated preferences for bob:"
|
||||
alice <## "full message deletion: off (you allow: no, contact allows: yes)"
|
||||
bob <## "alice updated preferences for you:"
|
||||
bob <## "full message deletion: off (you allow: default (yes), contact allows: no)"
|
||||
|
||||
testGetSetSMPServers :: IO ()
|
||||
testGetSetSMPServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
@@ -3315,23 +3437,21 @@ testGroupLink =
|
||||
concurrentlyN_
|
||||
[ do
|
||||
alice <## "bob (Bob): contact is connected"
|
||||
alice <## "bob invited to group #team via your group link",
|
||||
alice <## "bob invited to group #team via your group link"
|
||||
alice <## "#team: bob joined the group",
|
||||
do
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
bob <## "#team: alice invites you to join the group as member"
|
||||
bob <## "use /j team to accept"
|
||||
bob <## "#team: you joined the group"
|
||||
]
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link")])
|
||||
alice @@@ [("#team", "invited via your group link")] -- contacts connected via group link are not in chat previews
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")])
|
||||
-- contacts connected via group link are not in chat previews
|
||||
alice @@@ [("#team", "connected")]
|
||||
bob @@@ [("#team", "connected")]
|
||||
-- calling /_get chat api marks it as used and adds it to chat previews
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
alice @@@ [("@bob", ""), ("#team", "invited via your group link")]
|
||||
alice @@@ [("@bob", ""), ("#team", "connected")]
|
||||
alice <##> bob
|
||||
alice @@@ [("@bob", "hey"), ("#team", "invited via your group link")]
|
||||
bob ##> "/j team"
|
||||
concurrently_
|
||||
(alice <## "#team: bob joined the group")
|
||||
(bob <## "#team: you joined the group")
|
||||
alice @@@ [("@bob", "hey"), ("#team", "connected")]
|
||||
|
||||
-- user address doesn't interfere
|
||||
alice ##> "/ad"
|
||||
@@ -3349,25 +3469,18 @@ testGroupLink =
|
||||
cath ##> ("/c " <> gLink)
|
||||
cath <## "connection request sent!"
|
||||
alice <## "cath_1 (Catherine): accepting request to join group #team..."
|
||||
-- if contact existed it is merged
|
||||
concurrentlyN_
|
||||
[ do
|
||||
alice <## "cath_1 (Catherine): contact is connected"
|
||||
alice <## "cath_1 invited to group #team via your group link",
|
||||
alice <## "cath_1 invited to group #team via your group link"
|
||||
alice <## "contact cath_1 is merged into cath"
|
||||
alice <## "use @cath <message> to send messages"
|
||||
alice <## "#team: cath joined the group",
|
||||
do
|
||||
cath <## "alice_1 (Alice): contact is connected"
|
||||
cath <## "#team: alice_1 invites you to join the group as member"
|
||||
cath <## "use /j team to accept"
|
||||
]
|
||||
-- sending message to contact marks it as used
|
||||
alice @@@ [("@cath", "hey"), ("@bob", "hey"), ("#team", "invited via your group link")]
|
||||
alice #> "@cath_1 hello"
|
||||
cath <# "alice_1> hello"
|
||||
alice #$> ("/clear cath_1", id, "cath_1: all messages are removed locally ONLY")
|
||||
alice @@@ [("@cath_1", ""), ("@cath", "hey"), ("@bob", "hey"), ("#team", "invited via your group link")]
|
||||
cath ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: cath_1 joined the group",
|
||||
do
|
||||
cath <## "contact alice_1 is merged into alice"
|
||||
cath <## "use @alice <message> to send messages"
|
||||
cath <## "#team: you joined the group"
|
||||
cath <## "#team: member bob (Bob) is connected",
|
||||
do
|
||||
@@ -3377,14 +3490,14 @@ testGroupLink =
|
||||
alice #> "#team hello"
|
||||
concurrently_
|
||||
(bob <# "#team alice> hello")
|
||||
(cath <# "#team alice_1> hello")
|
||||
(cath <# "#team alice> hello")
|
||||
bob #> "#team hi there"
|
||||
concurrently_
|
||||
(alice <# "#team bob> hi there")
|
||||
(cath <# "#team bob> hi there")
|
||||
cath #> "#team hey team"
|
||||
concurrently_
|
||||
(alice <# "#team cath_1> hey team")
|
||||
(alice <# "#team cath> hey team")
|
||||
(bob <# "#team cath> hey team")
|
||||
|
||||
-- leaving team removes link
|
||||
@@ -3394,11 +3507,48 @@ testGroupLink =
|
||||
alice <## "#team: you left the group"
|
||||
alice <## "use /d #team to delete the group",
|
||||
bob <## "#team: alice left the group",
|
||||
cath <## "#team: alice_1 left the group"
|
||||
cath <## "#team: alice left the group"
|
||||
]
|
||||
alice ##> "/show link #team"
|
||||
alice <## "no group link, to create: /create link #team"
|
||||
|
||||
testGroupLinkContactUsed :: IO ()
|
||||
testGroupLinkContactUsed =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/show link #team"
|
||||
alice <## "no group link, to create: /create link #team"
|
||||
alice ##> "/create link #team"
|
||||
gLink <- getGroupLink alice "team" True
|
||||
alice ##> "/show link #team"
|
||||
_ <- getGroupLink alice "team" False
|
||||
alice ##> "/create link #team"
|
||||
alice <## "you already have link for this group, to show: /show link #team"
|
||||
bob ##> ("/c " <> gLink)
|
||||
bob <## "connection request sent!"
|
||||
alice <## "bob (Bob): accepting request to join group #team..."
|
||||
concurrentlyN_
|
||||
[ do
|
||||
alice <## "bob (Bob): contact is connected"
|
||||
alice <## "bob invited to group #team via your group link"
|
||||
alice <## "#team: bob joined the group",
|
||||
do
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
bob <## "#team: you joined the group"
|
||||
]
|
||||
-- sending/receiving a message marks contact as used
|
||||
alice @@@ [("#team", "connected")]
|
||||
bob @@@ [("#team", "connected")]
|
||||
alice #> "@bob hello"
|
||||
bob <# "alice> hello"
|
||||
alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
|
||||
alice @@@ [("@bob", ""), ("#team", "connected")]
|
||||
bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
|
||||
bob @@@ [("@alice", ""), ("#team", "connected")]
|
||||
|
||||
testGroupLinkIncognitoMembership :: IO ()
|
||||
testGroupLinkIncognitoMembership =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $
|
||||
@@ -3444,26 +3594,20 @@ testGroupLinkIncognitoMembership =
|
||||
[ do
|
||||
bob <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## "use /info cath to print out this incognito profile again"
|
||||
bob <## "cath invited to group #team via your group link",
|
||||
bob <## "cath invited to group #team via your group link"
|
||||
bob <## "#team: cath joined the group",
|
||||
do
|
||||
cath <## (bobIncognito <> ": contact is connected")
|
||||
cath <## ("#team: " <> bobIncognito <> " invites you to join the group as member")
|
||||
cath <## "use /j team to accept"
|
||||
]
|
||||
bob ?#> "@cath hi, I'm incognito"
|
||||
cath <# (bobIncognito <> "> hi, I'm incognito")
|
||||
cath #> ("@" <> bobIncognito <> " hey, I'm cath")
|
||||
bob ?<# "cath> hey, I'm cath"
|
||||
cath ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ bob <## "#team: cath joined the group",
|
||||
do
|
||||
cath <## "#team: you joined the group"
|
||||
cath <## "#team: member alice (Alice) is connected",
|
||||
do
|
||||
alice <## ("#team: " <> bobIncognito <> " added cath (Catherine) to the group (connecting...)")
|
||||
alice <## "#team: new member cath is connected"
|
||||
]
|
||||
bob ?#> "@cath hi, I'm incognito"
|
||||
cath <# (bobIncognito <> "> hi, I'm incognito")
|
||||
cath #> ("@" <> bobIncognito <> " hey, I'm cath")
|
||||
bob ?<# "cath> hey, I'm cath"
|
||||
-- dan joins incognito
|
||||
dan #$> ("/incognito on", id, "ok")
|
||||
dan ##> ("/c " <> gLink)
|
||||
@@ -3476,22 +3620,11 @@ testGroupLinkIncognitoMembership =
|
||||
[ do
|
||||
bob <## (danIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
|
||||
bob <## ("use /info " <> danIncognito <> " to print out this incognito profile again")
|
||||
bob <## (danIncognito <> " invited to group #team via your group link"),
|
||||
bob <## (danIncognito <> " invited to group #team via your group link")
|
||||
bob <## ("#team: " <> danIncognito <> " joined the group"),
|
||||
do
|
||||
dan <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> danIncognito)
|
||||
dan <## ("use /info " <> bobIncognito <> " to print out this incognito profile again")
|
||||
dan <## ("#team: " <> bobIncognito <> " invites you to join the group as member")
|
||||
dan <## ("use /j team to join incognito as " <> danIncognito)
|
||||
]
|
||||
dan #$> ("/incognito off", id, "ok")
|
||||
bob ?#> ("@" <> danIncognito <> " hi, I'm incognito")
|
||||
dan ?<# (bobIncognito <> "> hi, I'm incognito")
|
||||
dan ?#> ("@" <> bobIncognito <> " hey, me too")
|
||||
bob ?<# (danIncognito <> "> hey, me too")
|
||||
dan ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ bob <## ("#team: " <> danIncognito <> " joined the group"),
|
||||
do
|
||||
dan <## ("#team: you joined the group incognito as " <> danIncognito)
|
||||
dan
|
||||
<### [ "#team: member alice (Alice) is connected",
|
||||
@@ -3504,6 +3637,11 @@ testGroupLinkIncognitoMembership =
|
||||
cath <## ("#team: " <> bobIncognito <> " added " <> danIncognito <> " to the group (connecting...)")
|
||||
cath <## ("#team: new member " <> danIncognito <> " is connected")
|
||||
]
|
||||
dan #$> ("/incognito off", id, "ok")
|
||||
bob ?#> ("@" <> danIncognito <> " hi, I'm incognito")
|
||||
dan ?<# (bobIncognito <> "> hi, I'm incognito")
|
||||
dan ?#> ("@" <> bobIncognito <> " hey, me too")
|
||||
bob ?<# (danIncognito <> "> hey, me too")
|
||||
alice #> "#team hello"
|
||||
concurrentlyN_
|
||||
[ bob ?<# "#team alice> hello",
|
||||
@@ -3529,63 +3667,36 @@ testGroupLinkIncognitoMembership =
|
||||
cath <# ("#team " <> danIncognito <> "> how is it going?")
|
||||
]
|
||||
|
||||
testGroupLinkDeleteInvitedMemberNoBrokenItem :: IO ()
|
||||
testGroupLinkDeleteInvitedMemberNoBrokenItem =
|
||||
testSwitchContact :: IO ()
|
||||
testSwitchContact =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
alice ##> "/g team"
|
||||
alice <## "group #team is created"
|
||||
alice <## "use /a team <name> to add members"
|
||||
alice ##> "/create link #team"
|
||||
gLink <- getGroupLink alice "team" True
|
||||
bob ##> ("/c " <> gLink)
|
||||
bob <## "connection request sent!"
|
||||
alice <## "bob (Bob): accepting request to join group #team..."
|
||||
concurrentlyN_
|
||||
[ do
|
||||
alice <## "bob (Bob): contact is connected"
|
||||
alice <## "bob invited to group #team via your group link",
|
||||
do
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
bob <## "#team: alice invites you to join the group as member"
|
||||
bob <## "use /j team to accept"
|
||||
]
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link")])
|
||||
alice @@@ [("#team", "invited via your group link")]
|
||||
-- removing invited member who connected via group link does not leave broken chat item
|
||||
alice ##> "/rm team bob"
|
||||
alice <## "#team: you removed bob from the group"
|
||||
alice #$> ("/_get chat #1 count=100", chat, [])
|
||||
alice @@@ [("#team", "")]
|
||||
connectUsers alice bob
|
||||
alice #$> ("/switch bob", id, "ok")
|
||||
bob <## "alice started changing address for you"
|
||||
alice <## "bob: you started changing address"
|
||||
bob <## "alice changed address for you"
|
||||
alice <## "bob: you changed address"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [(1, "started changing address..."), (1, "you changed address")])
|
||||
bob #$> ("/_get chat @2 count=100", chat, [(0, "started changing address for you..."), (0, "changed address for you")])
|
||||
alice <##> bob
|
||||
alice @@@ [("@bob", "hey"), ("#team", "")]
|
||||
bob ##> "/j team"
|
||||
bob <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
|
||||
-- repeat request is prohibited because of the re-used XContactId, until contact is deleted
|
||||
bob ##> ("/c " <> gLink)
|
||||
bob <## "alice (Alice): contact already exists"
|
||||
bob ##> "/d alice"
|
||||
bob <## "alice: contact is deleted"
|
||||
bob ##> ("/c " <> gLink)
|
||||
bob <## "connection request sent!"
|
||||
alice <## "bob_1 (Bob): accepting request to join group #team..."
|
||||
concurrentlyN_
|
||||
[ do
|
||||
alice <## "bob_1 (Bob): contact is connected"
|
||||
alice <## "bob_1 invited to group #team via your group link",
|
||||
do
|
||||
bob <## "alice_1 (Alice): contact is connected"
|
||||
bob <## "#team_1 (team): alice_1 invites you to join the group as member"
|
||||
bob <## "use /j team_1 to accept"
|
||||
]
|
||||
bob ##> "/j team_1"
|
||||
concurrently_
|
||||
(alice <## "#team: bob_1 joined the group")
|
||||
(bob <## "#team_1: you joined the group")
|
||||
alice #> "#team hello"
|
||||
bob <# "#team_1 alice_1> hello"
|
||||
bob #> "#team_1 hi there"
|
||||
alice <# "#team bob_1> hi there"
|
||||
|
||||
testSwitchGroupMember :: IO ()
|
||||
testSwitchGroupMember =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
createGroup2 "team" alice bob
|
||||
alice #$> ("/switch #team bob", id, "ok")
|
||||
bob <## "#team: alice started changing address for you"
|
||||
alice <## "#team: you started changing address for bob"
|
||||
bob <## "#team: alice changed address for you"
|
||||
alice <## "#team: you changed address for bob"
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")])
|
||||
bob #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "started changing address for you..."), (0, "changed address for you")])
|
||||
alice #> "#team hey"
|
||||
bob <# "#team alice> hey"
|
||||
bob #> "#team hi"
|
||||
alice <# "#team bob> hi"
|
||||
|
||||
withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatContactConnected dbPrefix action =
|
||||
|
||||
@@ -7,6 +7,7 @@ import ChatTests
|
||||
import Control.Monad.Except
|
||||
import Simplex.Chat.Mobile
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Types (Profile (..))
|
||||
import Test.Hspec
|
||||
|
||||
mobileTests :: Spec
|
||||
@@ -92,7 +93,7 @@ testChatApi = withTmpFiles $ do
|
||||
let dbPrefix = testDBPrefix <> "1"
|
||||
f = chatStoreFile dbPrefix
|
||||
st <- createChatStore f "myKey" True
|
||||
Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile True
|
||||
Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile {preferences = Nothing} True
|
||||
Right cc <- chatMigrateInit dbPrefix "myKey"
|
||||
Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix ""
|
||||
Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey"
|
||||
|
||||
+26
-16
@@ -40,7 +40,8 @@ connReqData =
|
||||
ConnReqUriData
|
||||
{ crScheme = simplexChat,
|
||||
crAgentVRange = mkVersionRange 1 1,
|
||||
crSmpQueues = [queue]
|
||||
crSmpQueues = [queue],
|
||||
crClientData = Nothing
|
||||
}
|
||||
|
||||
testDhPubKey :: C.PublicKeyX448
|
||||
@@ -78,11 +79,17 @@ s #==# msg = do
|
||||
s #== msg
|
||||
s ==# msg
|
||||
|
||||
testChatPreferences :: Maybe Preferences
|
||||
testChatPreferences = Just Preferences {voice = Just Preference {allow = FAYes}, fullDelete = Nothing}
|
||||
|
||||
testGroupPreferences :: Maybe GroupPreferences
|
||||
testGroupPreferences = Just GroupPreferences {voice = Just GroupPreference {enable = FEOn}, fullDelete = Nothing}
|
||||
|
||||
testProfile :: Profile
|
||||
testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=")}
|
||||
testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), preferences = testChatPreferences}
|
||||
|
||||
testGroupProfile :: GroupProfile
|
||||
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", image = Nothing}
|
||||
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", image = Nothing, groupPreferences = testGroupPreferences}
|
||||
|
||||
decodeChatMessageTest :: Spec
|
||||
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
@@ -173,43 +180,46 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
"{\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
|
||||
#==# XFileCancel (SharedMsgId "\1\2\3\4")
|
||||
it "x.info" $
|
||||
"{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
"{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XInfo testProfile
|
||||
it "x.info with empty full name" $
|
||||
"{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\"}}}"
|
||||
#==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing}
|
||||
"{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, preferences = testChatPreferences}
|
||||
it "x.contact with xContactId" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
"{\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XContact testProfile (Just $ XContactId "\1\2\3\4")
|
||||
it "x.contact without XContactId" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
"{\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XContact testProfile Nothing
|
||||
it "x.contact with content null" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
==# XContact testProfile Nothing
|
||||
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=\"}}}"
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
==# XContact testProfile Nothing
|
||||
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-2%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, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile}
|
||||
"{\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing}
|
||||
it "x.grp.inv with group link id" $
|
||||
"{\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"}
|
||||
it "x.grp.acpt without incognito profile" $
|
||||
"{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
#==# 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=\"}}}}"
|
||||
"{\"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=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
|
||||
it "x.grp.mem.intro" $
|
||||
"{\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}}"
|
||||
"{\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
|
||||
it "x.grp.mem.inv" $
|
||||
"{\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
|
||||
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq}
|
||||
it "x.grp.mem.fwd" $
|
||||
"{\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}}"
|
||||
"{\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"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-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq}
|
||||
it "x.grp.mem.info" $
|
||||
"{\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
"{\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile
|
||||
it "x.grp.mem.con" $
|
||||
"{\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
|
||||
Reference in New Issue
Block a user