Compare commits

...

41 Commits

Author SHA1 Message Date
Evgeny Poberezkin 138dc7fe8f 4.3.2: terminal, ios (101), android (78) 2022-12-11 15:51:23 +00:00
Evgeny Poberezkin 0535d84719 website: add F-Droid key hash to open SimpleX links in the app 2022-12-11 15:39:11 +00:00
Evgeny Poberezkin f4447ffe89 readme: add BCH address for donations 2022-12-11 14:06:22 +00:00
Evgeny Poberezkin 146d5f99bc core: clear connection verification status (#1540) 2022-12-10 12:09:45 +00:00
Evgeny Poberezkin 73e5fff8f5 core: fix parser 2022-12-10 08:43:54 +00:00
Evgeny Poberezkin 33e7538172 core: group description (#1538)
* core: group description

* support multi-line welcome message

* fix
2022-12-10 08:27:32 +00:00
Stanislav Dmitrenko 49c9c501aa android: Fix for AddressAlreadyInUse exception (#1534)
* android: Fix for AddressAlreadyInUse exception

* simplify loop

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-09 22:15:50 +00:00
Evgeny Poberezkin a177dc5a13 core: refactor parser (#1537)
* core: refactor parser

* fix
2022-12-09 21:50:01 +00:00
Evgeny Poberezkin a4f207875f show /create link command when group is created (#1536) 2022-12-09 18:22:03 +00:00
JRoberts bcca0998d5 core: optimize group deletion (#1529) 2022-12-09 20:01:31 +04:00
Evgeny Poberezkin 95cc9e1e55 core: verify connection (#1530)
* core: verify connection

* update commands

* api to get/set verification code/status

* add migration

* refactor

* change command / response names

* reset verified status if code from agent doesn't match
2022-12-09 15:26:43 +00:00
sh ab5ae2d2cb build-android: add skip flag and update logic (#1525)
* build-android: add skip flag and update logic

* build-android: change equal
2022-12-08 08:55:37 +00:00
JRoberts 40a91a7273 android: version 4.3.1 (77) 2022-12-08 10:48:13 +04:00
JRoberts 1240b31df8 ios: version 4.3.1 (100) 2022-12-08 10:41:59 +04:00
Evgeny Poberezkin ff14730738 mobile, core: fix voice message reception in groups (#1524) 2022-12-07 22:18:22 +00:00
JRoberts 0cba3a4bb3 4.3.1 2022-12-07 21:10:45 +04:00
JRoberts 208f8a3346 android: version 4.3.1 (76) 2022-12-07 21:09:52 +04:00
JRoberts caa3efb9ed ios: version 4.3.1 (99) 2022-12-07 21:04:43 +04:00
JRoberts 4beb916754 ios: deleted item preview; android: refactor removeChatItem (#1523) 2022-12-07 20:46:38 +04:00
Stanislav Dmitrenko c1ee04eed1 android: Cancel notification after message deletion (#1512)
* android: Cancel notification after message deletion

* Improve

* Temporary chat item

* Better

* Changes

* cInfo, cItem

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-12-07 19:49:17 +04:00
JRoberts 0ad3bc9993 android: show open direct chat button for direct contacts (#1521) 2022-12-07 19:07:31 +04:00
JRoberts 9893aa665a core: don't mark contacts as used on api get chat (#1522) 2022-12-07 19:05:32 +04:00
JRoberts fda8836ab8 ios: show open direct chat button for direct contacts (#1518) 2022-12-07 17:30:15 +04:00
Evgeny Poberezkin 05fdd07409 website: add SHA256 of the key signing GitHub android APK to open links in the app 2022-12-07 10:27:31 +00:00
Evgeny Poberezkin fb8f5facd0 terminal: only set contact/group as active for terminal input if it is not muted (#1514) 2022-12-07 09:58:01 +00:00
Stanislav Dmitrenko 8bdb784a14 android: Added rememberSaveable in pref screens (fix merge) (#1517) 2022-12-07 09:57:23 +00:00
Stanislav Dmitrenko 5d785aad2e android: Added rememberSaveable in pref screens (#1509) 2022-12-06 21:04:15 +00:00
Stanislav Dmitrenko ce11d58a76 android: Saving prefs alert on exit with unsaved changes (#1508)
* android: Saving prefs alert on exit with unsaved changes

* DIfferent implementation for AlertDialog with long buttons

* Braces

* Change

* Alignment

* Rename

* small changes

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-06 20:48:15 +00:00
Evgeny Poberezkin 887b374bfc readme: add mastodon link 2022-12-06 19:53:08 +00:00
Evgeny Poberezkin 94dc967197 readme: update screenshots 2022-12-06 17:30:32 +00:00
JRoberts 4319a581ca core: more test cases checking deletion of unused contacts and incognito profiles (#1513) 2022-12-06 20:19:01 +04:00
JRoberts fb05218558 core: delete unused contacts after deleting group (#1503) 2022-12-06 17:12:39 +04:00
Evgeny Poberezkin edf2d02a0d blog: v4.3 release announcement (#1510)
* blog: v4.3 release announcement

* add images

* update image URIs

* update post

* typos

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* correction

* website preview, readme update

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-12-06 12:53:14 +00:00
Evgeny Poberezkin 87ba429dfd Merge branch 'stable' 2022-12-05 20:47:16 +00:00
Evgeny Poberezkin 7af1a7cf76 docs: update f-droid store info (#1507) 2022-12-05 20:46:11 +00:00
sh df619acdd4 build-android: update nix install (#1506) 2022-12-05 18:45:18 +00:00
Evgeny Poberezkin 503d0cd451 android: make backup disabled by default (#1505) 2022-12-05 15:05:56 +00:00
Stanislav Dmitrenko 1294a00ee7 android: Vibration pattern (#1504)
* android: Vibration pattern

* update pattern

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-05 14:56:37 +00:00
Stanislav Dmitrenko 0a8069ada2 android: Notification sound (#1468)
* android: fix full screen call notification (#1466)

* android: Closing call means canceling notification too

* show full screen call when screen is off OR locked

* make notification non-silent and set category

* remove call notification category

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* android: Notification sound

* Log

* Ringtone channel

* rename call channel

* Non-hideable headsUp notification and reject button

* Removed LockScreenCallChannel

* call channel name

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-05 13:13:48 +00:00
Evgeny Poberezkin c167f594b9 website: add .well-known folder to allow mobile apps process URLs 2022-12-05 11:28:22 +00:00
Evgeny Poberezkin ce5124594d blog: permalink for v4.3 post (#1499) 2022-12-04 18:18:23 +00:00
68 changed files with 1793 additions and 505 deletions
+16 -14
View File
@@ -5,8 +5,9 @@
[![build](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml/badge.svg?branch=stable)](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml)
[![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
[![Follow on Twitter](https://img.shields.io/twitter/follow/SimpleXChat?style=social)](https://twitter.com/SimpleXChat)
[![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat)
[![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex)
[![Follow on Twitter](https://img.shields.io/twitter/follow/SimpleXChat?style=social)](https://twitter.com/SimpleXChat)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
&nbsp;
@@ -85,16 +86,14 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent updates:
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration.](./blog/20221206-simplex-chat-v4.3-voice-messages.md)
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md)
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md)
[Aug 8, 2022. v3.1: secret chat groups, access via Tor, reduced battery and traffic usage, advanced network settings, etc.](./blog/20220808-simplex-chat-v3.1-chat-groups.md)
[Jul 11, 2022. v3.0: instant push notifications for iOS, e2e encrypted WebRTC audio/video calls, chat database export/import, privacy and performance improvements](./blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md)
[All updates](./blog)
## Make a private connection
@@ -103,7 +102,7 @@ You need to share a link or scan a QR code (in person or during a video call) to
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/conversation.png" alt="Make a private connection" width="594" height="360">
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
## :zap: Quick installation of a terminal app
@@ -187,19 +186,20 @@ If you are considering developing with SimpleX platform please get in touch for
- ✅ Chat database encryption.
- ✅ Automatic chat history deletion.
- ✅ Links to join groups and improve groups stability.
- ✅ Voice messages (with recipient opt-out per contact).
- ✅ Basic authentication for SMP servers (to authorize creating new queues).
- ✅ View deleted messages, full message deletion by sender (with recipient opt-in per contact).
- ✅ Block screenshots and view in recent apps.
- ✅ Advanced server configuration.
- 🏗 SMP queue redundancy and rotation (manual is supported).
- 🏗 Voice messages (with recipient opt-out per contact).
- 🏗 Basic authentication for SMP servers (to authorize creating new queues).
- View deleted messages, full message deletion by sender (with recipient opt-in per contact).
- Block screenshots and view in recent apps.
- 🏗 Contact verification via a separate out-of-band channel.
- 🏗 Ephemeral/disappearing/OTR conversations with the existing contacts.
- Optionally avoid re-using the same TCP session for multiple connections.
- Access password/pin (with optional alternative access password).
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Media server to optimize sending large files to groups.
- Video messages.
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- Multiple user profiles in the same chat database.
- Advanced server configuration.
- Feeds/broadcasts.
- Unconfirmed: disappearing messages (with recipient opt-in per-contact).
- Web widgets for custom interactivity in the chats.
@@ -246,8 +246,10 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 75
versionName "4.3"
versionCode 78
versionName "4.3.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
@@ -11,8 +11,7 @@ import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.*
import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
@@ -173,7 +172,18 @@ class SimplexApp: Application(), LifecycleEventObserver {
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
Log.d(TAG, "starting server")
val server = LocalServerSocket(socketName)
var server: LocalServerSocket? = null
for (i in 0..100) {
try {
server = LocalServerSocket(socketName + i)
break
} catch (e: IOException) {
Log.e(TAG, e.stackTraceToString())
}
}
if (server == null) {
throw Error("Unable to setup local server socket. Contact developers")
}
Log.d(TAG, "started server")
s.release()
val receiver = server.accept()
@@ -101,7 +101,7 @@ class ChatModel(val controller: ChatController) {
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = !contact.isIndirectContact && !contact.viaGroupLink)
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directContact)
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
@@ -215,6 +215,9 @@ class ChatModel(val controller: ChatController) {
}
fun removeChatItem(cInfo: ChatInfo, cItem: ChatItem) {
if (cItem.isRcvNew) {
decreaseCounterInChat(cInfo.id)
}
// update previews
val i = getChatIndex(cInfo.id)
val chat: Chat
@@ -222,7 +225,7 @@ class ChatModel(val controller: ChatController) {
chat = chats[i]
val pItem = chat.chatItems.lastOrNull()
if (pItem?.id == cItem.id) {
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
}
}
// remove from current chat
@@ -551,6 +554,7 @@ data class Contact(
val profile: LocalProfile,
val activeConn: Connection,
val viaGroup: Long? = null,
val contactUsed: Boolean,
val chatSettings: ChatSettings,
val userPreferences: ChatPreferences,
val mergedPreferences: ContactUserPreferences,
@@ -571,11 +575,8 @@ data class Contact(
override val image get() = profile.image
override val localAlias get() = profile.localAlias
val isIndirectContact: Boolean get() =
activeConn.connLevel > 0 || viaGroup != null
val viaGroupLink: Boolean get() =
activeConn.viaGroupLink
val directContact: Boolean get() =
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
val contactConnIncognito =
activeConn.customUserProfileId != null
@@ -586,6 +587,7 @@ data class Contact(
localDisplayName = "alice",
profile = LocalProfile.sampleData,
activeConn = Connection.sampleData,
contactUsed = true,
chatSettings = ChatSettings(true),
userPreferences = ChatPreferences.sampleData,
mergedPreferences = ContactUserPreferences.sampleData,
@@ -1202,6 +1204,26 @@ data class ChatItem (
file = null
)
}
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
val deletedItemDummy: ChatItem
get() = ChatItem(
chatDir = CIDirection.DirectRcv(),
meta = CIMeta(
itemId = TEMP_DELETED_CHAT_ITEM_ID,
itemTs = Clock.System.now(),
itemText = generalGetString(R.string.deleted_description),
itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(),
itemDeleted = false,
itemEdited = false,
editable = false
),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null,
file = null
)
}
}
@@ -25,9 +25,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
// DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION"
const val LockScreenCallChannel: String = "chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION"
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val CallNotificationId: Int = -1
private const val ChatIdKey: String = "chatId"
@@ -39,24 +39,29 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
init {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(NotificationChannel(LockScreenCallChannel, generalGetString(R.string.ntf_channel_calls_lockscreen), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel())
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
}
enum class NotificationAction {
ACCEPT_CONTACT_REQUEST
}
private fun callNotificationChannel(): NotificationChannel {
val callChannel = NotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls), NotificationManager.IMPORTANCE_HIGH)
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
val callChannel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
val attrs = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build()
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
Log.d(TAG,"callNotificationChannel sound: $soundUri")
callChannel.setSound(soundUri, attrs)
callChannel.enableVibration(true)
// the numbers below are explained here: https://developer.android.com/reference/android/os/Vibrator
// (wait, vibration duration, wait till off, wait till on again = ringtone mp3 duration - vibration duration - ~50ms lost somewhere)
callChannel.vibrationPattern = longArrayOf(250, 250, 0, 2600)
return callChannel
}
@@ -153,13 +158,14 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
fun notifyCallInvitation(invitation: RcvCallInvitation) {
if (SimplexApp.context.isAppOnForeground) return
val keyguardManager = getKeyguardManager(context)
Log.d(TAG,
"notifyCallInvitation pre-requests: device locked ${keyguardManager.isDeviceLocked}, " +
"notifyCallInvitation pre-requests: " +
"keyguard locked ${keyguardManager.isKeyguardLocked}, " +
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}"
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
"onForeground ${SimplexApp.context.isAppOnForeground}"
)
if (SimplexApp.context.isAppOnForeground) return
val contactId = invitation.contact.id
Log.d(TAG, "notifyCallInvitation $contactId")
val image = invitation.contact.image
@@ -169,15 +175,17 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, LockScreenCallChannel)
NotificationCompat.Builder(context, CallChannel)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSilent(false)
} else {
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, CallChannel)
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.contact.id))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, contactId))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, contactId, true))
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
}
val text = generalGetString(
@@ -206,8 +214,11 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
.setLargeIcon(largeIcon)
.setColor(0x88FFFF)
.setAutoCancel(true)
val notification = ntfBuilder.build()
// This makes notification sound and vibration repeat endlessly
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
with(NotificationManagerCompat.from(context)) {
notify(CallNotificationId, ntfBuilder.build())
notify(CallNotificationId, notification)
}
}
@@ -215,6 +226,8 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
manager.cancel(CallNotificationId)
}
fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() }
private fun hideSecrets(cItem: ChatItem) : String {
val md = cItem.formattedText
return if (md != null) {
@@ -228,16 +241,20 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
}
private fun chatPendingIntent(intentAction: String, chatId: String? = null): PendingIntent {
private fun chatPendingIntent(intentAction: String, chatId: String? = null, broadcast: Boolean = false): PendingIntent {
Log.d(TAG, "chatPendingIntent for $intentAction")
val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt()
var intent = Intent(context, MainActivity::class.java)
var intent = Intent(context, if (!broadcast) MainActivity::class.java else NtfActionReceiver::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
.setAction(intentAction)
if (chatId != null) intent = intent.putExtra(ChatIdKey, chatId)
return TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent)
getPendingIntent(uniqueInt, PendingIntent.FLAG_IMMUTABLE)
return if (!broadcast) {
TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent)
getPendingIntent(uniqueInt, PendingIntent.FLAG_IMMUTABLE)
}
} else {
PendingIntent.getBroadcast(SimplexApp.context, uniqueInt, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}
@@ -255,6 +272,12 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
acceptContactRequest(cInfo, SimplexApp.context.chatModel)
SimplexApp.context.chatModel.controller.ntfManager.cancelNotificationsForChat(chatId)
}
RejectCallAction -> {
val invitation = SimplexApp.context.chatModel.callInvitations[chatId]
if (invitation != null) {
SimplexApp.context.chatModel.callManager.endCall(invitation = invitation)
}
}
else -> {
Log.e(TAG, "Unknown action. Make sure you provide action from NotificationAction enum")
}
@@ -103,7 +103,7 @@ class AppPreferences(val context: Context) {
},
set = fun(mode: SimplexLinkMode) { _simplexLinkMode.set(mode.name) }
)
val privacyFullBackup = mkBoolPreference(SHARED_PREFS_PRIVACY_FULL_BACKUP, true)
val privacyFullBackup = mkBoolPreference(SHARED_PREFS_PRIVACY_FULL_BACKUP, false)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null)
@@ -196,7 +196,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
internal val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
internal const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
@@ -993,7 +993,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatModel.removeChat(r.connection.id)
}
is CR.ContactConnected -> {
if (!r.contact.viaGroupLink) {
if (r.contact.directContact) {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
@@ -1002,7 +1002,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
is CR.ContactConnecting -> {
if (!r.contact.viaGroupLink) {
if (r.contact.directContact) {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
@@ -1047,10 +1047,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
val cItem = r.chatItem.chatItem
chatModel.addChatItem(cInfo, cItem)
val file = cItem.file
if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV && appPrefs.privacyAcceptImages.get()) {
withApi { receiveFile(file.fileId) }
} else if (cItem.content.msgContent is MsgContent.MCVoice && file != null && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileSize > MAX_VOICE_SIZE_FOR_SENDING && appPrefs.privacyAcceptImages.get()) {
withApi { receiveFile(file.fileId) } // TODO check inlineFileMode != IFMSent
val mc = cItem.content.msgContent
if (file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) {
val acceptImages = appPrefs.privacyAcceptImages.get()
if ((mc is MsgContent.MCImage && acceptImages)
|| (mc is MsgContent.MCVoice && ((file.fileSize > MAX_VOICE_SIZE_FOR_SENDING && acceptImages) || cInfo is ChatInfo.Group))) {
withApi { receiveFile(file.fileId) } // TODO check inlineFileMode != IFMSent
}
}
if (cItem.showNotification && (!SimplexApp.context.isAppOnForeground || chatModel.chatId.value != cInfo.id)) {
ntfManager.notifyMessageReceived(cInfo, cItem)
@@ -1070,11 +1073,22 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
is CR.ChatItemUpdated ->
chatItemSimpleUpdate(r.chatItem)
is CR.ChatItemDeleted -> {
AudioPlayer.stop(r.deletedChatItem.chatItem)
val cInfo = r.deletedChatItem.chatInfo
val cItem = r.deletedChatItem.chatItem
AudioPlayer.stop(cItem)
val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
ntfManager.cancelNotificationsForChat(cInfo.id)
ntfManager.notifyMessageReceived(
cInfo.id,
cInfo.displayName,
generalGetString(if (r.toChatItem != null) R.string.marked_deleted_description else R.string.deleted_description)
)
}
if (r.toChatItem == null) {
chatModel.removeChatItem(r.deletedChatItem.chatInfo, r.deletedChatItem.chatItem)
chatModel.removeChatItem(cInfo, cItem)
} else {
chatModel.upsertChatItem(r.toChatItem.chatInfo, r.toChatItem.chatItem)
chatModel.upsertChatItem(cInfo, r.toChatItem.chatItem)
}
}
is CR.ReceivedGroupInvitation -> {
@@ -2202,6 +2216,7 @@ sealed class ContactFeatureAllowed {
}
}
@Serializable
data class ContactFeaturesAllowed(
val fullDelete: ContactFeatureAllowed,
val voice: ContactFeatureAllowed
@@ -1,5 +1,6 @@
package chat.simplex.app.views.call
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
@@ -43,8 +44,7 @@ class IncomingCallActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activity = this
setContent { IncomingCallActivityView(vm.chatModel, activity) }
setContent { IncomingCallActivityView(vm.chatModel) }
unlockForIncomingCall()
}
@@ -83,11 +83,12 @@ fun getKeyguardManager(context: Context): KeyguardManager =
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
@Composable
fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
fun IncomingCallActivityView(m: ChatModel) {
val switchingCall = m.switchingCall.value
val invitation = m.activeCallInvitation.value
val call = m.activeCall.value
val showCallView = m.showCallView.value
val activity = LocalContext.current as Activity
LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "IncomingCallActivityView: finishing activity")
@@ -105,20 +106,23 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m, activity)
IncomingCallLockScreenAlert(invitation, m)
}
}
}
}
@Composable
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel, activity: IncomingCallActivity) {
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
var callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) }
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
val callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) }
val context = LocalContext.current
DisposableEffect(Unit) {
onDispose {
// Cancel notification whatever happens next since otherwise sound from notification and from inside the app can co-exist
chatModel.controller.ntfManager.cancelCallNotification()
}
}
IncomingCallLockScreenAlertLayout(
invitation,
callOnLockScreen,
@@ -129,15 +133,14 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
},
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = {
SoundPlayer.shared.stop()
var intent = Intent(activity, MainActivity::class.java)
val intent = Intent(context, MainActivity::class.java)
.setAction(OpenChatAction)
.putExtra("chatId", invitation.contact.id)
activity.startActivity(intent)
activity.finish()
context.startActivity(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getKeyguardManager(activity).requestDismissKeyguard(activity, null)
getKeyguardManager(context).requestDismissKeyguard((context as Activity), null)
}
(context as Activity).finish()
}
)
}
@@ -63,8 +63,11 @@ fun ChatInfoView(
setContactAlias(chat.chatInfo.apiId, it, chatModel)
},
openPreferences = {
ModalManager.shared.showModal(true) {
ContactPreferencesView(chatModel, chatModel.currentUser.value ?: return@showModal, contact.contactId)
ModalManager.shared.showCustomModal { close ->
val user = chatModel.currentUser.value
if (user != null) {
ContactPreferencesView(chatModel, user, contact.contactId, close)
}
}
},
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -25,33 +26,45 @@ fun ContactPreferencesView(
m: ChatModel,
user: User,
contactId: Long,
close: () -> Unit,
) {
val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
val ct = contact.value ?: return
var featuresAllowed by remember(ct) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
var currentFeaturesAllowed by remember(ct) { mutableStateOf(featuresAllowed) }
ContactPreferencesLayout(
featuresAllowed,
currentFeaturesAllowed,
user,
ct,
applyPrefs = { prefs ->
featuresAllowed = prefs
},
reset = {
featuresAllowed = currentFeaturesAllowed
},
savePrefs = {
withApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(ct.contactId, prefs)
if (toContact != null) {
m.updateContact(toContact)
currentFeaturesAllowed = featuresAllowed
}
var featuresAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(ct.contactId, prefs)
if (toContact != null) {
m.updateContact(toContact)
currentFeaturesAllowed = featuresAllowed
}
afterSave()
}
}
ModalView(
close = {
if (featuresAllowed == currentFeaturesAllowed) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close)
},
)
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
ContactPreferencesLayout(
featuresAllowed,
currentFeaturesAllowed,
user,
ct,
applyPrefs = { prefs ->
featuresAllowed = prefs
},
reset = {
featuresAllowed = currentFeaturesAllowed
},
savePrefs = ::savePrefs,
)
}
}
@Composable
@@ -139,3 +152,13 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_contact),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -44,8 +44,8 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
selectedRole = selectedRole,
allowModifyMembers = allowModifyMembers,
openPreferences = {
ModalManager.shared.showModal(true) {
GroupPreferencesView(chatModel, groupInfo.id)
ModalManager.shared.showCustomModal { close ->
GroupPreferencesView(chatModel, groupInfo.id, close)
}
},
inviteMembers = {
@@ -65,10 +65,11 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
},
openPreferences = {
ModalManager.shared.showModal(true) {
ModalManager.shared.showCustomModal { close ->
GroupPreferencesView(
chatModel,
chat.id
chat.id,
close
)
}
},
@@ -49,20 +49,26 @@ fun GroupMemberInfoView(
connStats,
newRole,
developerTools,
openDirectChat = {
getContactChat = { chatModel.getContactChat(it) },
knownDirectChat = {
withApi {
val oldChat = chatModel.getContactChat(member.memberContactId ?: return@withApi)
if (oldChat != null) {
openChat(oldChat.chatInfo, chatModel)
} else {
var newChat = chatModel.controller.apiGetChat(ChatType.Direct, member.memberContactId) ?: return@withApi
chatModel.chatItems.clear()
chatModel.chatItems.addAll(it.chatItems)
chatModel.chatId.value = it.chatInfo.id
closeAll()
}
},
newDirectChat = {
withApi {
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
if (c != null) {
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
newChat = newChat.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
val newChat = c.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
chatModel.addChat(newChat)
chatModel.chatItems.clear()
chatModel.chatId.value = newChat.id
closeAll()
}
closeAll()
}
},
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
@@ -114,7 +120,9 @@ fun GroupMemberInfoLayout(
connStats: ConnectionStats?,
newRole: MutableState<GroupMemberRole>,
developerTools: Boolean,
openDirectChat: () -> Unit,
getContactChat: (Long) -> Chat?,
knownDirectChat: (Chat) -> Unit,
newDirectChat: (Long) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
@@ -133,11 +141,20 @@ fun GroupMemberInfoLayout(
}
SectionSpacer()
if (member.memberContactId != null && groupInfo.fullGroupPreferences.directMessages.enable == GroupFeatureEnabled.ON) {
SectionView {
OpenChatButton(openDirectChat)
val contactId = member.memberContactId
if (contactId != null) {
val chat = getContactChat(contactId)
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directContact) {
SectionView {
OpenChatButton(onClick = { knownDirectChat(chat) })
}
SectionSpacer()
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
SectionView {
OpenChatButton(onClick = { newDirectChat(contactId) })
}
SectionSpacer()
}
SectionSpacer()
}
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
@@ -302,7 +319,9 @@ fun PreviewGroupMemberInfoLayout() {
connStats = null,
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
developerTools = false,
openDirectChat = {},
getContactChat = { Chat.sampleData },
knownDirectChat = {},
newDirectChat = {},
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},
@@ -11,43 +11,53 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimplexGreen
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun GroupPreferencesView(m: ChatModel, chatId: String) {
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
val groupInfo = remember { derivedStateOf { (m.getChat(chatId)?.chatInfo as? ChatInfo.Group)?.groupInfo } }
val gInfo = groupInfo.value ?: return
var preferences by remember(gInfo) { mutableStateOf(gInfo.fullGroupPreferences) }
var currentPreferences by remember(gInfo) { mutableStateOf(preferences) }
GroupPreferencesLayout(
preferences,
currentPreferences,
gInfo,
applyPrefs = { prefs ->
preferences = prefs
},
reset = {
preferences = currentPreferences
},
savePrefs = {
withApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val gInfo = m.controller.apiUpdateGroup(gInfo.groupId, gp)
if (gInfo != null) {
m.updateGroup(gInfo)
currentPreferences = preferences
}
var preferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.fullGroupPreferences) }
var currentPreferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(preferences) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val gInfo = m.controller.apiUpdateGroup(gInfo.groupId, gp)
if (gInfo != null) {
m.updateGroup(gInfo)
currentPreferences = preferences
}
afterSave()
}
}
ModalView(
close = {
if (preferences == currentPreferences) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close)
},
)
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
GroupPreferencesLayout(
preferences,
currentPreferences,
gInfo,
applyPrefs = { prefs ->
preferences = prefs
},
reset = {
preferences = currentPreferences
},
savePrefs = ::savePrefs,
)
}
}
@Composable
@@ -130,3 +140,13 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -5,9 +5,10 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Dialog
import chat.simplex.app.R
import chat.simplex.app.TAG
@@ -93,6 +94,41 @@ class AlertManager {
}
}
fun showAlertDialogStacked(
title: String,
text: String? = null,
confirmText: String = generalGetString(R.string.ok),
onConfirm: (() -> Unit)? = null,
dismissText: String = generalGetString(R.string.cancel_verb),
onDismiss: (() -> Unit)? = null,
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = { Text(title) },
text = alertText,
buttons = {
Column(
Modifier.fillMaxWidth().padding(horizontal = 8.dp).padding(top = 16.dp, bottom = 2.dp),
horizontalAlignment = Alignment.End
) {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
},
)
}
}
fun showAlertMsg(
title: String, text: String? = null,
confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null
@@ -128,4 +164,4 @@ class AlertManager {
companion object {
val shared = AlertManager()
}
}
}
@@ -29,7 +29,10 @@ import androidx.core.content.FileProvider
import androidx.core.text.HtmlCompat
import chat.simplex.app.*
import chat.simplex.app.model.CIFile
import chat.simplex.app.model.json
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
@@ -462,3 +465,9 @@ val LongRange.Companion.saver
save = { it.value.first to it.value.last },
restore = { mutableStateOf(it.first..it.second) }
)
/* Make sure that T class has @Serializable annotation */
inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
save = { json.encodeToString(it) },
restore = { json.decodeFromString(it) }
)
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -19,33 +20,40 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun PreferencesView(m: ChatModel, user: User) {
var preferences by remember { mutableStateOf(user.fullPreferences) }
var currentPreferences by remember { mutableStateOf(preferences) }
PreferencesLayout(
preferences,
currentPreferences,
applyPrefs = { prefs ->
preferences = prefs
},
reset = {
preferences = currentPreferences
},
savePrefs = {
withApi {
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
val updatedProfile = m.controller.apiUpdateProfile(newProfile)
if (updatedProfile != null) {
val updatedUser = user.copy(
profile = updatedProfile.toLocalProfile(user.profile.profileId),
fullPreferences = preferences
)
currentPreferences = preferences
m.currentUser.value = updatedUser
}
fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
var preferences by rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(user.fullPreferences) }
var currentPreferences by rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(preferences) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
val updatedProfile = m.controller.apiUpdateProfile(newProfile)
if (updatedProfile != null) {
val updatedUser = user.copy(
profile = updatedProfile.toLocalProfile(user.profile.profileId),
fullPreferences = preferences
)
currentPreferences = preferences
m.currentUser.value = updatedUser
}
afterSave()
}
}
ModalView(
close = {
if (preferences == currentPreferences) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close)
},
)
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
PreferencesLayout(
preferences,
currentPreferences,
applyPrefs = { preferences = it },
reset = { preferences = currentPreferences },
savePrefs = ::savePrefs,
)
}
}
@Composable
@@ -107,3 +115,13 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_contacts),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -116,7 +116,7 @@ fun SettingsLayout(
SectionDivider()
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped)
SectionDivider()
ChatPreferencesItem(showSettingsModal)
ChatPreferencesItem(showCustomModal)
}
SectionSpacer()
@@ -239,14 +239,14 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable fun ChatPreferencesItem(showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit))) {
SettingsActionItem(
Icons.Outlined.ToggleOn,
stringResource(R.string.chat_preferences),
click = {
withApi {
showSettingsModal {
PreferencesView(it, it.currentUser.value ?: return@showSettingsModal)
showCustomModal { m, close ->
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
}()
}
}
@@ -115,7 +115,6 @@
<!-- Notification channels -->
<string name="ntf_channel_messages">SimpleX Chat Nachrichten</string>
<string name="ntf_channel_calls">SimpleX Chat Anrufe</string>
<string name="ntf_channel_calls_lockscreen">SimpleX Chat Anrufe (Sperrbildschirm)</string>
<!-- Notifications -->
<string name="settings_notifications_mode_title">Benachrichtigungsdienst</string>
@@ -476,9 +475,11 @@
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
<string name="edit_image">Bild bearbeiten</string>
<string name="delete_image">Bild löschen</string>
<string name="save_and_notify_contact">Speichern (und Kontakt benachrichtigen)</string>
<string name="save_and_notify_contacts">Speichern (und Kontakte benachrichtigen)</string>
<string name="save_and_notify_group_members">Speichern (und Gruppenmitglieder benachrichtigen)</string>
<string name="save_preferences_question">Präferenzen speichern?</string>
<string name="save_and_notify_contact">Speichern und Kontakt benachrichtigen</string>
<string name="save_and_notify_contacts">Speichern und Kontakte benachrichtigen</string>
<string name="save_and_notify_group_members">Speichern und Gruppenmitglieder benachrichtigen</string>
<string name="exit_without_saving">Beenden ohne Speichern</string>
<!-- Welcome Prompts - WelcomeView.kt -->
<string name="you_control_your_chat">Sie haben volle Kontrolle über Ihren Chat!</string>
@@ -115,7 +115,6 @@
<!-- Notification channels -->
<string name="ntf_channel_messages">SimpleX Chat сообщения</string>
<string name="ntf_channel_calls">SimpleX Chat звонки</string>
<string name="ntf_channel_calls_lockscreen">SimpleX Chat звонки (экран блокировки)</string>
<!-- Notifications -->
<string name="settings_notifications_mode_title">Сервис уведомлений</string>
@@ -473,9 +472,11 @@
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профиль хранится на вашем устройстве и отправляется только вашим контактам.\n\n<xliff:g id="appName">SimpleX</xliff:g> серверы не могут получить доступ к вашему профилю.</string>
<string name="edit_image">Поменять аватар</string>
<string name="delete_image">Удалить аватар</string>
<string name="save_and_notify_contact">Сохранить (и уведомить контакт)</string>
<string name="save_and_notify_contacts">Сохранить (и послать обновление контактам)</string>
<string name="save_and_notify_group_members">Сохранить (и уведомить членов группы)</string>
<string name="save_preferences_question">Сохранить предпочтения?</string>
<string name="save_and_notify_contact">Сохранить и уведомить контакт</string>
<string name="save_and_notify_contacts">Сохранить и уведомить контакты</string>
<string name="save_and_notify_group_members">Сохранить и уведомить членов группы</string>
<string name="exit_without_saving">Выйти без сохранения</string>
<!-- Welcome Prompts - WelcomeView.kt -->
<string name="you_control_your_chat">Вы котролируете ваш чат!</string>
@@ -115,7 +115,6 @@
<!-- Notification channels -->
<string name="ntf_channel_messages">SimpleX Chat messages</string>
<string name="ntf_channel_calls">SimpleX Chat calls</string>
<string name="ntf_channel_calls_lockscreen">SimpleX Chat calls (lock screen)</string>
<!-- Notifications -->
<string name="settings_notifications_mode_title">Notification service</string>
@@ -476,9 +475,11 @@
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts.\n\n<xliff:g id="appName">SimpleX</xliff:g> servers cannot see your profile.</string>
<string name="edit_image">Edit image</string>
<string name="delete_image">Delete image</string>
<string name="save_and_notify_contact">Save (and notify contact)</string>
<string name="save_and_notify_contacts">Save (and notify contacts)</string>
<string name="save_and_notify_group_members">Save (and notify group members)</string>
<string name="save_preferences_question">Save preferences?</string>
<string name="save_and_notify_contact">Save and notify contact</string>
<string name="save_and_notify_contacts">Save and notify contacts</string>
<string name="save_and_notify_group_members">Save and notify group members</string>
<string name="exit_without_saving">Exit without saving</string>
<!-- Welcome Prompts - WelcomeView.kt -->
<string name="you_control_your_chat">You control your chat!</string>
+14 -7
View File
@@ -101,7 +101,7 @@ final class ChatModel: ObservableObject {
}
func updateContact(_ contact: Contact) {
updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact && !contact.viaGroupLink)
updateChat(.direct(contact: contact), addMissing: contact.directContact)
}
func updateGroup(_ groupInfo: GroupInfo) {
@@ -236,16 +236,19 @@ final class ChatModel: ObservableObject {
}
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if cItem.isRcvNew {
decreaseUnreadCounter(cInfo)
}
// update previews
if let chat = getChat(cInfo.id) {
if let pItem = chat.chatItems.last, pItem.id == cItem.id {
chat.chatItems = [cItem]
chat.chatItems = [ChatItem.deletedItemDummy()]
}
}
// remove from current chat
if chatId == cInfo.id {
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
if reversedChatItems[i].isRcvNew() == true {
if reversedChatItems[i].isRcvNew {
NtfManager.shared.decNtfBadgeCount()
}
_ = withAnimation {
@@ -342,9 +345,7 @@ final class ChatModel: ObservableObject {
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update preview
if let i = getChatIndex(cInfo.id) {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
}
decreaseUnreadCounter(cInfo)
// update current chat
if chatId == cInfo.id, let j = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
reversedChatItems[j].meta.itemStatus = .rcvRead
@@ -352,6 +353,12 @@ final class ChatModel: ObservableObject {
}
}
func decreaseUnreadCounter(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
}
}
func totalUnreadCount() -> Int {
chats.reduce(0, { count, chat in count + chat.chatStats.unreadCount })
}
@@ -418,7 +425,7 @@ final class ChatModel: ObservableObject {
var unreadBelow = 0
while i < reversedChatItems.count - 1 && !itemsInView.contains(reversedChatItems[i].viewId) {
totalBelow += 1
if reversedChatItems[i].isRcvNew() {
if reversedChatItems[i].isRcvNew {
unreadBelow += 1
}
i += 1
+11 -16
View File
@@ -917,7 +917,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
case let .contactConnectionDeleted(connection):
m.removeChat(connection.id)
case let .contactConnected(contact, _):
if !contact.viaGroupLink {
if contact.directContact {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
@@ -925,7 +925,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
NtfManager.shared.notifyContactConnected(contact)
}
case let .contactConnecting(contact):
if !contact.viaGroupLink {
if contact.directContact {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
@@ -972,20 +972,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
m.addChatItem(cInfo, cItem)
if case .image = cItem.content.msgContent,
let file = cItem.file,
file.fileSize <= MAX_IMAGE_SIZE,
UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) {
Task {
await receiveFile(fileId: file.fileId)
}
} else if case .voice = cItem.content.msgContent, // TODO check inlineFileMode != IFMSent
let file = cItem.file,
file.fileSize <= MAX_IMAGE_SIZE,
file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND,
UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) {
Task {
await receiveFile(fileId: file.fileId)
if let file = cItem.file,
let mc = cItem.content.msgContent,
file.fileSize <= MAX_IMAGE_SIZE {
let acceptImages = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
if (mc.isImage && acceptImages)
|| (mc.isVoice && ((file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND && acceptImages) || cInfo.chatType == .group)) {
Task {
await receiveFile(fileId: file.fileId) // TODO check inlineFileMode != IFMSent
}
}
}
if cItem.showNotification {
+1 -1
View File
@@ -221,7 +221,7 @@ struct ChatView: View {
.onAppear {
itemsInView.insert(ci.viewId)
loadChatItems(cInfo, ci, proxy)
if ci.isRcvNew() {
if ci.isRcvNew {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) {
Task {
@@ -42,9 +42,16 @@ struct GroupMemberInfoView: View {
groupMemberInfoHeader(member)
.listRowBackground(Color.clear)
if let contactId = member.memberContactId, groupInfo.fullGroupPreferences.directMessages.enable == .on {
Section {
openDirectChatButton(contactId)
if let contactId = member.memberContactId {
if let chat = chatModel.getContactChat(contactId),
chat.chatInfo.contact?.directContact ?? false {
Section {
knownDirectChatButton(chat)
}
} else if groupInfo.fullGroupPreferences.directMessages.on {
Section {
newDirectChatButton(contactId)
}
}
}
@@ -112,26 +119,30 @@ struct GroupMemberInfoView: View {
}
}
func openDirectChatButton(_ contactId: Int64) -> some View {
func knownDirectChatButton(_ chat: Chat) -> some View {
Button {
var chat = chatModel.getContactChat(contactId)
if chat == nil {
do {
chat = try apiGetChat(type: .direct, id: contactId)
if let chat = chat {
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
chat.serverInfo = Chat.ServerInfo(networkStatus: .connected)
chatModel.addChat(chat)
}
} catch let error {
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
}
dismissAllSheets(animated: true)
DispatchQueue.main.async {
chatModel.chatId = chat.id
}
if let chat = chat {
} label: {
Label("Send direct message", systemImage: "message")
}
}
func newDirectChatButton(_ contactId: Int64) -> some View {
Button {
do {
let chat = try apiGetChat(type: .direct, id: contactId)
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
chat.serverInfo = Chat.ServerInfo(networkStatus: .connected)
chatModel.addChat(chat)
dismissAllSheets(animated: true)
DispatchQueue.main.async {
chatModel.chatId = chat.id
}
} catch let error {
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
}
} label: {
Label("Send direct message", systemImage: "message")
+28 -28
View File
@@ -137,11 +137,11 @@
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; };
644EFFEC293B5B1800525D5B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFE7293B5B1700525D5B /* libgmpxx.a */; };
644EFFED293B5B1800525D5B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFE8293B5B1700525D5B /* libgmp.a */; };
644EFFEE293B5B1800525D5B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFE9293B5B1800525D5B /* libffi.a */; };
644EFFEF293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFEA293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a */; };
644EFFF0293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFEB293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a */; };
644EFFF62941BD6900525D5B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFF12941BD6800525D5B /* libffi.a */; };
644EFFF72941BD6900525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFF22941BD6800525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW-ghc8.10.7.a */; };
644EFFF82941BD6900525D5B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFF32941BD6800525D5B /* libgmp.a */; };
644EFFF92941BD6900525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFF42941BD6800525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW.a */; };
644EFFFA2941BD6900525D5B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFF52941BD6900525D5B /* libgmpxx.a */; };
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
@@ -348,11 +348,11 @@
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = "<group>"; };
644EFFE7293B5B1700525D5B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
644EFFE8293B5B1700525D5B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
644EFFE9293B5B1800525D5B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
644EFFEA293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a"; sourceTree = "<group>"; };
644EFFEB293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a"; sourceTree = "<group>"; };
644EFFF12941BD6800525D5B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
644EFFF22941BD6800525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW-ghc8.10.7.a"; sourceTree = "<group>"; };
644EFFF32941BD6800525D5B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
644EFFF42941BD6800525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW.a"; sourceTree = "<group>"; };
644EFFF52941BD6900525D5B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
@@ -398,13 +398,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
644EFFEC293B5B1800525D5B /* libgmpxx.a in Frameworks */,
644EFFF72941BD6900525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW-ghc8.10.7.a in Frameworks */,
644EFFF92941BD6900525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW.a in Frameworks */,
644EFFF82941BD6900525D5B /* libgmp.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
644EFFED293B5B1800525D5B /* libgmp.a in Frameworks */,
644EFFF0293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a in Frameworks */,
644EFFEE293B5B1800525D5B /* libffi.a in Frameworks */,
644EFFF62941BD6900525D5B /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
644EFFEF293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a in Frameworks */,
644EFFFA2941BD6900525D5B /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -460,11 +460,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
644EFFE9293B5B1800525D5B /* libffi.a */,
644EFFE8293B5B1700525D5B /* libgmp.a */,
644EFFE7293B5B1700525D5B /* libgmpxx.a */,
644EFFEA293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a */,
644EFFEB293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a */,
644EFFF12941BD6800525D5B /* libffi.a */,
644EFFF32941BD6800525D5B /* libgmp.a */,
644EFFF52941BD6900525D5B /* libgmpxx.a */,
644EFFF22941BD6800525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW-ghc8.10.7.a */,
644EFFF42941BD6800525D5B /* libHSsimplex-chat-4.3.1-5rS6G9uR2Qu1YtPYDlH5CW.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1259,7 +1259,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 98;
CURRENT_PROJECT_VERSION = 101;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1280,7 +1280,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.3;
MARKETING_VERSION = 4.3.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1301,7 +1301,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 98;
CURRENT_PROJECT_VERSION = 101;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1322,7 +1322,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.3;
MARKETING_VERSION = 4.3.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1380,7 +1380,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 98;
CURRENT_PROJECT_VERSION = 101;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1393,7 +1393,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.3;
MARKETING_VERSION = 4.3.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -1410,7 +1410,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 98;
CURRENT_PROJECT_VERSION = 101;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1423,7 +1423,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.3;
MARKETING_VERSION = 4.3.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
+40 -15
View File
@@ -802,6 +802,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var profile: LocalProfile
public var activeConn: Connection
public var viaGroup: Int64?
public var contactUsed: Bool
public var chatSettings: ChatSettings
public var userPreferences: Preferences
public var mergedPreferences: ContactUserPreferences
@@ -817,12 +818,8 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var image: String? { get { profile.image } }
public var localAlias: String { profile.localAlias }
public var isIndirectContact: Bool {
activeConn.connLevel > 0 || viaGroup != nil
}
public var viaGroupLink: Bool {
activeConn.viaGroupLink
public var directContact: Bool {
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
}
public var contactConnIncognito: Bool {
@@ -834,6 +831,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
localDisplayName: "alice",
profile: LocalProfile.sampleData,
activeConn: Connection.sampleData,
contactUsed: true,
chatSettings: ChatSettings.defaults,
userPreferences: Preferences.sampleData,
mergedPreferences: ContactUserPreferences.sampleData,
@@ -1382,7 +1380,7 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public func isRcvNew() -> Bool {
public var isRcvNew: Bool {
if case .rcvNew = meta.itemStatus { return true }
return false
}
@@ -1460,7 +1458,7 @@ public struct ChatItem: Identifiable, Decodable {
content: .sndMsgContent(msgContent: .text(text)),
quotedItem: quotedItem,
file: file
)
)
}
public static func getVoiceMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "voice.m4a", fileSize: Int64 = 65536, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem {
@@ -1470,7 +1468,7 @@ public struct ChatItem: Identifiable, Decodable {
content: .rcvMsgContent(msgContent: .voice(text: text, duration: 30)),
quotedItem: nil,
file: CIFile.getSample(fileName: fileName, fileSize: fileSize, fileStatus: fileStatus)
)
)
}
public static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem {
@@ -1480,7 +1478,7 @@ public struct ChatItem: Identifiable, Decodable {
content: .rcvMsgContent(msgContent: .file(text)),
quotedItem: nil,
file: CIFile.getSample(fileName: fileName, fileSize: fileSize, fileStatus: fileStatus)
)
)
}
public static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem {
@@ -1490,7 +1488,7 @@ public struct ChatItem: Identifiable, Decodable {
content: .rcvDeleted(deleteMode: .cidmBroadcast),
quotedItem: nil,
file: nil
)
)
}
public static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem {
@@ -1500,7 +1498,7 @@ public struct ChatItem: Identifiable, Decodable {
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: fromMsgId, toMsgId: toMsgId)),
quotedItem: nil,
file: nil
)
)
}
public static func getGroupInvitationSample (_ status: CIGroupInvitationStatus = .pending) -> ChatItem {
@@ -1510,7 +1508,7 @@ public struct ChatItem: Identifiable, Decodable {
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: status), memberRole: .admin),
quotedItem: nil,
file: nil
)
)
}
public static func getGroupEventSample () -> ChatItem {
@@ -1520,7 +1518,7 @@ public struct ChatItem: Identifiable, Decodable {
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
quotedItem: nil,
file: nil
)
)
}
public static func getChatFeatureSample(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> ChatItem {
@@ -1531,7 +1529,27 @@ public struct ChatItem: Identifiable, Decodable {
content: content,
quotedItem: nil,
file: nil
)
)
}
public static func deletedItemDummy() -> ChatItem {
ChatItem(
chatDir: CIDirection.directRcv,
meta: CIMeta(
itemId: -1,
itemTs: .now,
itemText: NSLocalizedString("deleted", comment: "deleted chat item"),
itemStatus: .rcvRead,
createdAt: .now,
updatedAt: .now,
itemDeleted: false,
itemEdited: false,
editable: false
),
content: .rcvDeleted(deleteMode: .cidmBroadcast),
quotedItem: nil,
file: nil
)
}
}
@@ -1788,6 +1806,13 @@ public enum MsgContent {
}
}
public var isImage: Bool {
switch self {
case .image: return true
default: return false
}
}
var cmdString: String {
switch self {
case let .text(text): return "text \(text)"
@@ -0,0 +1,139 @@
---
layout: layouts/article.html
title: "SimpleX Chat reviews and v4.3 released with instant voice messages, irreversible deletion of sent messages and improved server configuration."
date: 2022-12-06
image: images/20221206-voice.png
imageBottom: true
previewBody: blog_previews/20221206.html
permalink: "/blog/20221206-simplex-chat-v4.3-voice-messages.html"
---
# SimpleX Chat reviews and v4.3 released with instant voice messages, irreversible deletion of sent messages and improved server configuration.
**Published:** Dec 6, 2022
## SimpleX Chat reviews
Since we published [the security assessment of SimpleX Chat](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations:
- Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat).
- Mike Kuketz a well-known security expert published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de).
- Supernova published [the review](https://supernova.tilde.team/detailed_reviews.html#simplex) and increased [SimpleX Chat recommendation ratings](https://supernova.tilde.team/messengers.html).
## What's new in v4.3
- [instant voice messages!](#instant-voice-messages)
- [irreversible deletion of sent messages for all recipients](#irreversible-message-deletion)
- [improved SMP server configuration and support for server passwords](#smp-servers-configuration-and-password)
- [privacy and security improvements](#privacy-and-security-improvements):
- protect app screen in recent apps and prevent screenshots
- improved privacy and security of SimpleX invitation links in the app
- optional Android app data backup
- optionally allow direct messages between group members
### Instant voice messages
<img src="./images/20221206-voice.png" width="288">
Voice messages, unlike normal files, are sent instantly, in the existing connection with your contact and without acceptance from the recipient. For this reason we limited the size of voice messages to ~92.5kb (an equivalent of 6 messages), that limits the duration to 30 seconds on iOS and to ~42 seconds on Android (the size is different because of different encoders), with an average sound quality. The voice messages are sent in MP4AAC format that is natively supported both on iOS and on Android, and you can play voice message files outside of SimpleX Chat app.
Users who do not want to receive voice messages can disable them, either globally, for all contacts, or for each contact independently. Please note that the global preference change will only affect the contacts where you shared your main profile (not incognito contacts) and where you didn't change the preference for the particular contact. Groups have a separate policy that allows disabling voice messages for all members (they are allowed by default). The owner can set this policy when creating a group or later, via Group preferences page.
### Irreversible message deletion
<img src="./images/20221206-deleted1.png" width="288"> &nbsp;&nbsp; <img src="./images/20221206-deleted2.png" width="288">
When you receive email, you have full confidence that the sender cannot delete their email from your mailbox after you received it. And it seems correct in the end, this is your device, and nobody should be able to delete any data from it.
Most existing messengers made an opposite decision the senders can irreversibly delete their messages from the recipients' devices after they were delivered, whether recipients agree to that or not. And it seems correct too - this is your message, you should be able to delete it, at least for a limited time; that the message is on the recipient device doesn't change your ownership of this message.
While both these statements appear correct, at least to some people, they simply cannot both be correct at the same time, as they contradict each other - either one or both of them must be wrong. This appears to be a very polarising subject, and [the polls](https://mastodon.social/@simplex/109461879089268041) [I made](https://www.reddit.com/r/SimpleXChat/comments/zdam11/poll_irreversible_message_deletion_by_sender_what/) [yesterday](https://twitter.com/epoberezkin/status/1599797374389727233) [show it](https://www.linkedin.com/feed/update/urn:li:activity:7005564342502842368/) - the votes are split evenly.
You may want to be able to delete your messages even after they are received to protect your privacy and security, and you want the communication product you use to enforce it. But you may also have many reason to disagree to the deletion of messages on your device for several different reasons:
- it may be a business context, and either your organisation policy or a compliance requirement is that every message you receive must be preserved for some time.
- these messages may contain threat or abuse and you want to keep them as a proof.
- you may have paid for the the message (e.g., it can be a consulting report), and you don't want it to suddenly disappear before you had a chance to store it outside of the conversation.
Instead of taking any side in this choice, we decided to allow to change this behaviour either globally or separately for each contact or group. That makes SimpleX Chat unique, being suitable both for the communication contexts where email is traditionally used and in informal or privacy sensitive contexts, that would allow the senders to delete messages irreversibly, provided that the recipients agree to that.
In any case, the senders can never be 100% certain that the message is deleted from the recipient's device - recipient can be running a modified client that does not honour the conversation setting, and there is no way to ascertain which code your contact runs on their device.
If irreversible message deletion is not allowed in the conversation, the senders can still mark their messages as deleted, and it would show "mark deleted" placeholder in the conversation. The recipients can then both reveal the content of the original message and fully delete it on their devices.
### SMP servers configuration and password
<img src="./images/20221206-server1.png" width="288"> &nbsp;&nbsp; <img src="./images/20221206-server2.png" width="288"> &nbsp;&nbsp; <img src="./images/20221206-server3.png" width="288">
When you self-host your own SMP server you may want to make it public so that anybody can use it to receive messages. But many users want to host their private servers, so that only they and their friends can use them to receive the messages.
v4.0 of SMP server and the new version of the apps adds support for server passwords. It is chosen randomly when you initialize the new server, and if you already have a server you can change it. Anybody can still message you, it doesn't require knowing the password, and the links you share do not include it, but to be able to receive the messages you need to know a server address that includes the password. In a way, it is similar to how basic authentication works in HTTP, and how browsers support the URIs with included credentials.
The new server configuration section now allows to test your servers before you start using them, and you can also share your server address via QR code, so that your friends or your team can use them too, without the need to copy paste the addresses.
You can read how to install and configure SMP servers in [this guide](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md).
### Privacy and security improvements
#### Protect app screen
<img src="./images/20221206-protect.png" width="330">
It is now enabled by default, but you can disable it via settings.
iOS app only hides the app screen in the recent apps, Android app in addition to that also prevents the screenshots.
This is not the security measure for the senders, and we made it optional, as the recipient could circumvent it anyway this is for you to protect your app screen when you give your phone to somebody.
#### Privacy and security of SimpleX invitation links
Previously, when you sent somebody an invitation link, a contact address or a group link, they would take half a screen in the chat and they could open in the browser in some cases. Also, as these links are quire large, it is not easy to see if the page domain is maliciously replaced, what SMP server the connection would go through or what kind of link it is.
This version instead of showing the full link shows a short description, and it replaces a public web address with an internal URI scheme that the app uses (simplex:/) such links open directly in the app. There is an option to show the full link, if you need it, and even to open it in the browser from the app, but in this case if this link is not using https://simplex.chat website it will show as red to highlight it.
### Optional Android app data backup
The previous version always backed up app data in the way it was configured by the system. Now you can override it from inside the app, preventing the backup even if it's enabled by the system settings. This version requires disabling it manually, we will make it disabled by default in the next release (v4.3.1).
### Direct messages between group members
The new version does not allow them by default, but it can be enabled by group owners in the group settings when the group is created or at any later moment.
## SimpleX platform
Some links to answer the most common questions:
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
[Technical details and limitations](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md#privacy-technical-details-and-limitations).
[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions).
Please also see the information on our [new website](https://simplex.chat) - it also answers all these questions.
## Help us with donations
Huge thank you to everybody who donated to SimpleX Chat!
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, makes a big difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in many crypto-currencies.
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- please let us know, via GitHub issue or chat, if you want to make a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
Evgeny
SimpleX Chat founder
+16
View File
@@ -1,5 +1,21 @@
# Blog
Dec 12, 2022 [SimpleX Chat reviews and v4.3 released]
November reviews:
- [Privacy Guides](https://www.privacyguides.org/real-time-communication/#simplex-chat) recommendations.
- [Review by Mike Kuketz](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/).
- [The messenger matrix](https://www.messenger-matrix.de).
- [Supernova review](https://supernova.tilde.team/detailed_reviews.html#simplex) and [messenger ratings](https://supernova.tilde.team/messengers.html).
v4.3 is released:
- instant voice messages!
- irreversible deletion of sent messages for all recipients
- improved SMP server configuration and support for server passwords
- privacy and security improvements: protect app screen, SimpleX links security, etc.
Nov 8, 2022 [Security audit by Trail of Bits, the new website and v4.2 released](./20221108-simplex-chat-v4.2-security-audit-new-website.md)
_"Have you been audited or should we just ignore you?"_
Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

+1 -1
View File
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: e4842f4f47fb60ef8843dbce6fd43dec96f157d2
tag: fb21d9836e07706c7498baa967f932cb11b818e5
source-repository-package
type: git
@@ -1,23 +1,38 @@
<h1>SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!</h1>
<p><strong>Full privacy of your identity, profile, contacts and metadata</strong>: unlike any other existing messaging platform, SimpleX uses no phone numbers or any other identifiers assigned to the users - not even random numbers. This protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers.</p>
<p><strong>Complete protection against spam and abuse</strong>: as you have no identifier on SimpleX platform, you cannot be contacted unless you share a one-time invitation link or an optional temporary user address. Read more.</p>
<p><strong>Full ownership, control and security of your data</strong>: SimpleX stores all user data on client devices, the messages are only held temporarily on SimpleX relay servers until they are received.</p>
<p><strong>Decentralized network</strong>: you can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers.</p>
<p><a href="https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html" target="_blank">Security assessment</a> was done by Trail of Bits in November 2022.</p>
<p>SimpleX Chat features:</p>
<ul>
<li>end-to-end encrypted messages, with editing, replies and deletion of messages.</li>
<li>sending end-to-end encrypted images and files.</li>
<li>single-use and long-term user addresses.</li>
<li>secret chat groups - only group members know it exists and who is the member.</li>
<li>end-to-end encrypted audio and video calls.</li>
<li>private instant notifications.</li>
<li>portable chat profile - you can transfer your chat contacts and history to another device (terminal or mobile).</li>
</ul>
<p>SimpleX Chat advantages:</p>
<ul>
<li><strong>Full privacy of your identity, profile, contacts and metadata</strong>: unlike any other existing messaging platform, SimpleX uses no phone numbers or any other identifiers assigned to the users - not even random numbers. This protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers.</li>
<li><strong>Complete protection against spam and abuse</strong>: as you have no identifier on SimpleX platform, you cannot be contacted unless you share a one-time invitation link or an optional temporary user address.</li>
<li><strong>Full ownership, control and security of your data</strong>: SimpleX stores all user data on client devices, the messages are only held temporarily on SimpleX relay servers until they are received.</li>
<li><strong>Decentralized network</strong>: you can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers.</li>
</ul>
<p>You can connect to anybody you know via link or scan QR code (in the video call or in person) and start sending messages instantly - no emails, phone numbers or passwords needed.</p>
<p>Your profile and contacts are only stored in the app on your device - our servers do not have access to this information.</p>
<p>All messages are end-to-end encrypted using open-source double-ratchet protocol; the messages are routed via our servers using open-source SimpleX Messaging Protocol.</p>
<p>The app sends local notifications only when messages or connection requests arrive - the app checks for the new messages every 10-15 min, but if you stop using the app it may stop checking for the new messages.</p>
<p>Please send us any questions via the app (connect to the team via settings!), <a href="mailto:chat@simplex.chat?subject=SimpleX Chat on F-Droid">email us</a> or submit an <a href="https://github.com/simplex-chat/simplex-chat/issues" target="_blank">issue on GitHub</a>.</p>
<p>Please send us any questions via the app (connect to the team via settings!), email chat@simplex.chat or submit issues on GitHub (https://github.com/simplex-chat/simplex-chat/issues)</p>
<p>Source code: https://github.com/simplex-chat/simplex-chat</p>
<p>Follow us on Twitter (@SimpleXChat) and Reddit (r/SimpleXChat/) for the latest updates.</p>
<p>Follow us on Mastodon (<a href="https://mastodon.social/@simplex" target="_blank">@simplex@mastodon.social</a>), Twitter (<a href="https://twitter.com/SimpleXChat" target="_blank">@SimpleXChat</a>) and Reddit (<a href="https://www.reddit.com/r/SimpleXChat/" target="_blank">r/SimpleXChat</a>) for the latest updates.</p>
<p>Once you install SimpleX Chat, join the group of users via <a href="https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D" target="_blank">this link</a> to share your ideas and feedback.</p>
<p>Source code: <a href="https://github.com/simplex-chat/simplex-chat#readme" target="_blank">https://github.com/simplex-chat/simplex-chat</a></p>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 924 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 4.3.0
version: 4.3.2
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
+25 -17
View File
@@ -4,8 +4,7 @@ set -eu
u="$USER"
tmp=$(mktemp -d -t)
source="github:simplex-chat/simplex-chat"
commit="$1"
folder="$tmp/simplex-chat"
commands="nix git curl gradle zip unzip zipalign"
nix_install() {
@@ -13,9 +12,9 @@ nix_install() {
[ ! -d /nix ] && sudo sh -c "mkdir -p /nix && chown -R $u /nix"
# Install nix
nix_ver="nix-2.10.3"
nix_ver="nix-2.11.1"
nix_url="https://releases.nixos.org/nix/$nix_ver/install"
nix_hash="2e96a9c4abb5648a805480e8679de3d9fecff30453603f11c26bb4e7176c7ebe"
nix_hash="4569a01dc5f62056f29f3195673bc3242fc70bf2474927fb5d8549c4d997402d"
curl -sSf "$nix_url" -o "$tmp/nix-install"
printf "%s %s" "$nix_hash" "$tmp/nix-install" | sha256sum -c
@@ -30,14 +29,11 @@ nix_setup() {
}
git_setup() {
# Clone simplex
git clone https://github.com/simplex-chat/simplex-chat "$tmp/simplex-chat"
# Switch to nix-android branch
git -C "$tmp/simplex-chat" checkout "$commit"
git -C "$folder" checkout "$commit"
# Create missing folders
mkdir -p "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a"
mkdir -p "$folder/apps/android/app/src/main/cpp/libs/arm64-v8a"
}
checks() {
@@ -59,6 +55,10 @@ checks() {
esac
done
[ "$folder" != "." ] && {
git clone https://github.com/simplex-chat/simplex-chat "$folder"
}
if [ -n "$commands_failed" ]; then
commands_failed=${commands_failed% *}
printf "%s is not found in your \$PATH. Please install them and re-run the script.\n" "$commands_failed"
@@ -70,19 +70,19 @@ checks() {
build() {
# Build simplex lib
nix build "$source/$commit#hydraJobs.aarch64-android:lib:simplex-chat.x86_64-linux"
unzip -o "$PWD/result/pkg-aarch64-android-libsimplex.zip" -d "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a"
nix build "$folder#hydraJobs.aarch64-android:lib:simplex-chat.x86_64-linux"
unzip -o "$PWD/result/pkg-aarch64-android-libsimplex.zip" -d "$folder/apps/android/app/src/main/cpp/libs/arm64-v8a"
# Build android suppprt lib
nix build "$source/$commit#hydraJobs.aarch64-android:lib:support.x86_64-linux"
unzip -o "$PWD/result/pkg-aarch64-android-libsupport.zip" -d "$tmp/simplex-chat/apps/android/app/src/main/cpp/libs/arm64-v8a"
nix build "$folder#hydraJobs.aarch64-android:lib:support.x86_64-linux"
unzip -o "$PWD/result/pkg-aarch64-android-libsupport.zip" -d "$folder/apps/android/app/src/main/cpp/libs/arm64-v8a"
sed -i.bak 's/${extract_native_libs}/true/' "$tmp/simplex-chat/apps/android/app/src/main/AndroidManifest.xml"
sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/android/app/src/main/AndroidManifest.xml"
gradle -p "$tmp/simplex-chat/apps/android/" clean build assembleRelease
gradle -p "$folder/apps/android/" clean build assembleRelease
mkdir -p "$tmp/android"
unzip -oqd "$tmp/android/" "$tmp/simplex-chat/apps/android/app/build/outputs/apk/release/app-release-unsigned.apk"
unzip -oqd "$tmp/android/" "$folder/apps/android/app/build/outputs/apk/release/app-release-unsigned.apk"
(cd "$tmp/android" && zip -rq5 "$tmp/simplex-chat.apk" . && zip -rq0 "$tmp/simplex-chat.apk" resources.arsc res)
@@ -94,10 +94,18 @@ final() {
}
main() {
while getopts ":s" opt; do
case $opt in
s) folder="." ;;
*) printf "Flag '-%s' doesn't exist.\n" "$OPTARG"; exit 1 ;;
esac
done
shift $(( $OPTIND - 1 ))
commit="$1"; shift 1
checks
git_setup
build
final
}
main
main "$@"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."e4842f4f47fb60ef8843dbce6fd43dec96f157d2" = "191h2v5jcn51haj0mi5y5pm8x8fi9pz49ydxwwzqr3m6zjp21ngg";
"https://github.com/simplex-chat/simplexmq.git"."fb21d9836e07706c7498baa967f932cb11b818e5" = "0dl08ag38d1azzil1xxi6xrzqwfcv550wi5kjdmxn4h820icl2ja";
"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";
+4 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 4.3.0
version: 4.3.2
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -65,6 +65,9 @@ library
Simplex.Chat.Migrations.M20221115_server_cfg
Simplex.Chat.Migrations.M20221129_delete_group_feature_items
Simplex.Chat.Migrations.M20221130_delete_item_deleted
Simplex.Chat.Migrations.M20221209_verified_connection
Simplex.Chat.Migrations.M20221210_idxs
Simplex.Chat.Migrations.M20221211_group_description
Simplex.Chat.Mobile
Simplex.Chat.Options
Simplex.Chat.ProfileGenerator
+158 -70
View File
@@ -280,8 +280,7 @@ processChatCommand = \case
APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of
-- TODO optimize queries calculating ChatStats, currently they're disabled
CTDirect -> do
directChat@Chat {chatInfo} <- withStore (\db -> getDirectChat db user cId pagination search)
case chatInfo of DirectChat ct@Contact {contactUsed} -> unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
directChat <- withStore (\db -> getDirectChat db user cId pagination search)
pure . CRApiChat $ AChat SCTDirect directChat
CTGroup -> CRApiChat . AChat SCTGroup <$> withStore (\db -> getGroupChat db user cId pagination search)
CTContactRequest -> pure $ chatCmdError "not implemented"
@@ -345,7 +344,7 @@ processChatCommand = \case
if isVoice mc && not (groupFeatureAllowed GFVoice gInfo)
then pure $ chatCmdError $ "feature not allowed " <> T.unpack (groupFeatureToText GFVoice)
else do
(fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length ms)
(fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length $ filter memberCurrent ms)
(msgContainer, quotedItem_) <- prepareMsg fileInvitation_ membership
msg@SndMessage {sharedMsgId} <- sendGroupMessage gInfo ms (XMsgNew msgContainer)
mapM_ (sendGroupFileInline ms sharedMsgId) ft_
@@ -525,7 +524,19 @@ processChatCommand = \case
withStore' $ \db -> deleteGroupConnectionsAndFiles db user gInfo members
withStore' $ \db -> deleteGroupItemsAndMembers db user gInfo members
withStore' $ \db -> deleteGroup db user gInfo
let contactIds = mapMaybe memberContactId members
forM_ contactIds $ \ctId ->
deleteUnusedContact ctId `catchError` (toView . CRChatError)
pure $ CRGroupDeletedUser gInfo
where
deleteUnusedContact contactId = do
ct <- withStore $ \db -> getContact db user contactId
unless (directContact ct) $ do
ctGroupId <- withStore' $ \db -> checkContactHasGroups db user ct
when (isNothing ctGroupId) $ do
conns <- withStore $ \db -> getContactConnections db userId ct
forM_ conns $ \conn -> deleteAgentConnectionAsync user conn `catchError` \_ -> pure ()
withStore' $ \db -> deleteContactWithoutGroups db user ct
CTContactRequest -> pure $ chatCmdError "not supported"
APIClearChat (ChatRef cType chatId) -> withUser $ \user -> case cType of
CTDirect -> do
@@ -747,24 +758,53 @@ processChatCommand = \case
case memberConnId m of
Just connId -> withAgent (\a -> switchConnectionAsync a "" connId) $> CRCmdOk
_ -> throwChatError CEGroupMemberNotActive
APIGetContactCode contactId -> withUser $ \user -> do
ct@Contact {activeConn = conn@Connection {connId}} <- withStore $ \db -> getContact db user contactId
code <- getConnectionCode (contactConnId ct)
ct' <- case contactSecurityCode ct of
Just SecurityCode {securityCode}
| sameVerificationCode code securityCode -> pure ct
| otherwise -> do
withStore' $ \db -> setConnectionVerified db user connId Nothing
pure (ct :: Contact) {activeConn = conn {connectionCode = Nothing}}
_ -> pure ct
pure $ CRContactCode ct' code
APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do
(g, m@GroupMember {activeConn}) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId
case activeConn of
Just conn@Connection {connId} -> do
code <- getConnectionCode $ aConnId conn
m' <- case memberSecurityCode m of
Just SecurityCode {securityCode}
| sameVerificationCode code securityCode -> pure m
| otherwise -> do
withStore' $ \db -> setConnectionVerified db user connId Nothing
pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}}
_ -> pure m
pure $ CRGroupMemberCode g m' code
_ -> throwChatError CEGroupMemberNotActive
APIVerifyContact contactId code -> withUser $ \user -> do
Contact {activeConn} <- withStore $ \db -> getContact db user contactId
verifyConnectionCode user activeConn code
APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do
GroupMember {activeConn} <- withStore $ \db -> getGroupMember db user gId gMemberId
case activeConn of
Just conn -> verifyConnectionCode user conn code
_ -> throwChatError CEGroupMemberNotActive
ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do
chatId <- case cType of
CTDirect -> withStore $ \db -> getContactIdByName db user name
CTGroup -> withStore $ \db -> getGroupIdByName db user name
_ -> throwChatError $ CECommandError "not supported"
processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ ChatSettings ntfOn
ContactInfo cName -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db user cName
processChatCommand $ APIContactInfo contactId
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
ContactInfo cName -> withContactName cName APIContactInfo
GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo
SwitchContact cName -> withContactName cName APISwitchContact
SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember
GetContactCode cName -> withContactName cName APIGetContactCode
GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode
VerifyContact cName code -> withContactName cName (`APIVerifyContact` code)
VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code
ChatHelp section -> pure $ CRChatHelp section
Welcome -> withUser $ pure . CRWelcome
AddContact -> withUser $ \User {userId} -> withChatLock "addContact" . procCmd $ do
@@ -791,12 +831,8 @@ processChatCommand = \case
ConnectSimplex -> withUser $ \user ->
-- [incognito] generate profile to send
connectViaContact user adminContactReq
DeleteContact cName -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db user cName
processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId)
ClearContact cName -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db user cName
processChatCommand $ APIClearChat (ChatRef CTDirect contactId)
DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user)
CreateMyAddress -> withUser $ \User {userId} -> withChatLock "createMyAddress" . procCmd $ do
(connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact Nothing
@@ -926,7 +962,7 @@ processChatCommand = \case
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent gEvent) Nothing Nothing
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci
pure CRMemberRoleUser {groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole}
APIRemoveMember groupId memberId -> withUser $ \user@User {userId} -> do
APIRemoveMember groupId memberId -> withUser $ \user -> do
Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user groupId
case find ((== memberId) . groupMemberId') members of
Nothing -> throwChatError CEGroupMemberNotFound
@@ -944,10 +980,8 @@ 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 ->
checkGroupMemberHasItems db user m >>= \case
Just _ -> updateGroupMemberStatus db userId m GSMemRemoved
Nothing -> deleteGroupMember db user m
-- undeleted "member connected" chat item will prevent deletion of member record
deleteOrUpdateMemberRecord 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
@@ -957,6 +991,7 @@ processChatCommand = \case
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci
-- TODO delete direct connections that were unused
deleteGroupLink' user gInfo `catchError` \_ -> pure ()
-- member records are not deleted to keep history
forM_ members $ deleteMemberConnection user
withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft
pure $ CRLeftMemberUser gInfo {membership = membership {memberStatus = GSMemLeft}}
@@ -967,12 +1002,8 @@ processChatCommand = \case
JoinGroup gName -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APIJoinGroup groupId
MemberRole gName groupMemberName memRole -> do
(groupId, groupMemberId) <- getGroupAndMemberId gName groupMemberName
processChatCommand $ APIMemberRole groupId groupMemberId memRole
RemoveMember gName groupMemberName -> do
(groupId, groupMemberId) <- getGroupAndMemberId gName groupMemberName
processChatCommand $ APIRemoveMember groupId groupMemberId
MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole
RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember
LeaveGroup gName -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APILeaveGroup groupId
@@ -989,9 +1020,12 @@ processChatCommand = \case
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
g <- withStore $ \db -> getGroup db user groupId
runUpdateGroupProfile user g p'
UpdateGroupProfile gName profile -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APIUpdateGroupProfile groupId profile
UpdateGroupNames gName GroupProfile {displayName, fullName} ->
updateGroupProfileByName gName $ \p -> p {displayName, fullName}
ShowGroupProfile gName -> withUser $ \user ->
CRGroupProfile <$> withStore (\db -> getGroupInfoByName db user gName)
UpdateGroupDescription gName description ->
updateGroupProfileByName gName $ \p -> p {description}
APICreateGroupLink groupId -> withUser $ \user -> withChatLock "createGroupLink" $ do
gInfo@GroupInfo {membership = membership@GroupMember {memberRole = userRole}} <- withStore $ \db -> getGroupInfo db user groupId
when (userRole < GRAdmin) $ throwChatError CEGroupUserRole
@@ -1086,10 +1120,9 @@ processChatCommand = \case
ct@Contact {userPreferences} <- withStore $ \db -> getContactByName db user cName
let prefs' = setPreference f allowed_ $ Just userPreferences
updateContactPrefs user ct prefs'
SetGroupFeature f gName enabled -> withUser $ \user -> do
g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> getGroup db user =<< getGroupIdByName db user gName
let p' = p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
runUpdateGroupProfile user g p'
SetGroupFeature f gName enabled ->
updateGroupProfileByName gName $ \p ->
p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
QuitChat -> liftIO exitSuccess
ShowVersion -> pure $ CRVersionInfo versionNumber
DebugLocks -> do
@@ -1125,6 +1158,24 @@ processChatCommand = \case
withStoreChanged a = checkChatStopped $ a >> setStoreChanged $> CRCmdOk
checkStoreNotChanged :: m ChatResponse -> m ChatResponse
checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged)
withContactName :: ContactName -> (ContactId -> ChatCommand) -> m ChatResponse
withContactName cName cmd = withUser $ \user ->
withStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd
withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> m ChatResponse
withMemberName gName mName cmd = withUser $ \user ->
getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd
getConnectionCode :: ConnId -> m Text
getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId)
verifyConnectionCode :: User -> Connection -> Maybe Text -> m ChatResponse
verifyConnectionCode user conn@Connection {connId} (Just code) = do
code' <- getConnectionCode $ aConnId conn
let verified = sameVerificationCode code code'
when verified . withStore' $ \db -> setConnectionVerified db user connId $ Just code'
pure $ CRConnectionVerified verified code'
verifyConnectionCode user conn@Connection {connId} _ = do
code' <- getConnectionCode $ aConnId conn
withStore' $ \db -> setConnectionVerified db user connId Nothing
pure $ CRConnectionVerified False code'
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg)
@@ -1212,6 +1263,11 @@ processChatCommand = \case
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat g') ci
createGroupFeatureChangedItems user cd CISndGroupFeature p p'
pure $ CRGroupUpdated g g' Nothing
updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> m ChatResponse
updateGroupProfileByName gName update = withUser $ \user -> do
g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db ->
getGroupIdByName db user gName >>= getGroup db user
runUpdateGroupProfile user g $ update p
isReady :: Contact -> Bool
isReady ct =
let s = connStatus $ activeConn (ct :: Contact)
@@ -1243,8 +1299,8 @@ processChatCommand = \case
_ -> throwChatError CEFileNotReceived {fileId}
where
forward = processChatCommand . sendCommand chatName
getGroupAndMemberId :: GroupName -> ContactName -> m (GroupId, GroupMemberId)
getGroupAndMemberId gName groupMemberName = withUser $ \user -> do
getGroupAndMemberId :: User -> GroupName -> ContactName -> m (GroupId, GroupMemberId)
getGroupAndMemberId user gName groupMemberName =
withStore $ \db -> do
groupId <- getGroupIdByName db user gName
groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName
@@ -1912,7 +1968,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
GCHostMember -> do
toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected}
createGroupFeatureItems gInfo m
let GroupInfo {groupProfile = GroupProfile {description}} = gInfo
memberConnectedChatItem gInfo m
forM_ description $ groupDescriptionChatItem gInfo m
setActive $ ActiveG gName
showToast ("#" <> gName) "you are connected to group"
GCInviteeMember -> do
@@ -2188,6 +2246,10 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
-- ts should be broker ts but we don't have it for CON
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing
groupDescriptionChatItem :: GroupInfo -> GroupMember -> Text -> m ()
groupDescriptionChatItem gInfo m descr =
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do
memberConnectedChatItem gInfo m
@@ -2225,12 +2287,15 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
let ExtMsgContent content fileInvitation_ = mcExtMsgContent mc
if isVoice content && not (featureAllowed CFVoice forContact ct)
then void $ newChatItem (CIRcvChatFeatureRejected CFVoice) Nothing
then do
void $ newChatItem (CIRcvChatFeatureRejected CFVoice) Nothing
setActive $ ActiveC c
else do
ciFile_ <- processFileInvitation fileInvitation_ content $ \db -> createRcvFileTransfer db userId ct
ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_
when (enableNtfs chatSettings) $ showMsgToast (c <> "> ") content formattedText
setActive $ ActiveC c
when (enableNtfs chatSettings) $ do
showMsgToast (c <> "> ") content formattedText
setActive $ ActiveC c
where
newChatItem ciContent ciFile_ = do
ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta ciContent ciFile_
@@ -2296,8 +2361,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
ciFile_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m
ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_
let g = groupName' gInfo
when (enableNtfs chatSettings) $ showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
setActive $ ActiveG g
when (enableNtfs chatSettings) $ do
showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
setActive $ ActiveG g
where
newChatItem ciContent ciFile_ = do
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta ciContent ciFile_
@@ -2792,23 +2858,26 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
if memberId (membership :: GroupMember) == memId
then checkRole membership $ do
deleteGroupLink' user gInfo `catchError` \_ -> pure ()
-- member records are not deleted to keep history
forM_ members $ deleteMemberConnection user
deleteMember membership RGEUserDeleted
withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved
deleteMemberItem RGEUserDeleted
toView $ CRDeletedMemberUser gInfo {membership = membership {memberStatus = GSMemRemoved}} m
else case find (sameMemberId memId) members of
Nothing -> messageError "x.grp.mem.del with unknown member ID"
Just member@GroupMember {groupMemberId, memberProfile} ->
checkRole member $ do
deleteMemberConnection user member
deleteMember member $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)
-- undeleted "member connected" chat item will prevent deletion of member record
deleteOrUpdateMemberRecord user member
deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)
toView $ CRDeletedMember gInfo m member {memberStatus = GSMemRemoved}
where
checkRole GroupMember {memberRole} a
| senderRole < GRAdmin || senderRole < memberRole =
messageError "x.grp.mem.del with insufficient member permissions"
| otherwise = a
deleteMember member gEvent = do
withStore' $ \db -> updateGroupMemberStatus db userId member GSMemRemoved
deleteMemberItem gEvent = do
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent gEvent) Nothing
groupMsgToView gInfo m ci msgMeta
@@ -2818,6 +2887,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> MsgMeta -> m ()
xGrpLeave gInfo m msg msgMeta = do
deleteMemberConnection user m
-- member record is not deleted to allow creation of "member left" chat item
withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent RGEMemberLeft) Nothing
groupMsgToView gInfo m ci msgMeta
@@ -2830,6 +2900,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
members <- getGroupMembers db user gInfo
updateGroupMemberStatus db userId membership GSMemGroupDeleted
pure members
-- member records are not deleted to keep history
forM_ ms $ deleteMemberConnection user
ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent RGEGroupDeleted) Nothing
groupMsgToView gInfo m ci msgMeta
@@ -2987,6 +3058,13 @@ deleteMemberConnection user GroupMember {activeConn} = do
deleteAgentConnectionAsync user conn `catchError` \_ -> pure ()
withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
deleteOrUpdateMemberRecord :: ChatMonad m => User -> GroupMember -> m ()
deleteOrUpdateMemberRecord user@User {userId} member =
withStore' $ \db ->
checkGroupMemberHasItems db user member >>= \case
Just _ -> updateGroupMemberStatus db userId member GSMemRemoved
Nothing -> deleteGroupMember db user member
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent = do
if connStatus == ConnReady || connStatus == ConnSndReady
@@ -3317,7 +3395,7 @@ chatCommandP =
"/_accept " *> (APIAcceptContact <$> A.decimal),
"/_reject " *> (APIRejectContact <$> A.decimal),
"/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP),
("/call @" <|> "/call ") *> (SendCallInvitation <$> displayName <*> pure defaultCallType),
"/call " *> char_ '@' *> (SendCallInvitation <$> displayName <*> pure defaultCallType),
"/_call reject @" *> (APIRejectCall <$> A.decimal),
"/_call offer @" *> (APISendCallOffer <$> A.decimal <* A.space <*> jsonP),
"/_call answer @" *> (APISendCallAnswer <$> A.decimal <* A.space <*> jsonP),
@@ -3359,34 +3437,43 @@ chatCommandP =
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
"/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal),
"/_info @" *> (APIContactInfo <$> A.decimal),
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
("/info @" <|> "/info " <|> "/i @" <|> "/i ") *> (ContactInfo <$> displayName),
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName),
("/info " <|> "/i ") *> char_ '@' *> (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),
"/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
"/switch " *> char_ '@' *> (SwitchContact <$> displayName),
"/_get code @" *> (APIGetContactCode <$> A.decimal),
"/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal),
"/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> textP)),
"/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> textP)),
"/code " *> char_ '@' *> (GetContactCode <$> displayName),
"/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* char_ '@' <*> displayName),
"/verify " *> char_ '@' *> (VerifyContact <$> displayName <*> optional (A.space *> textP)),
"/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> optional (A.space *> textP)),
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
("/help messages" <|> "/hm") $> ChatHelp HSMessages,
("/help settings" <|> "/hs") $> ChatHelp HSSettings,
("/help" <|> "/h") $> ChatHelp HSMain,
("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile),
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile),
"/_group " *> (NewGroup <$> jsonP),
("/add #" <|> "/add " <|> "/a #" <|> "/a ") *> (AddMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName <*> memberRole),
("/join #" <|> "/join " <|> "/j #" <|> "/j ") *> (JoinGroup <$> displayName),
("/member role #" <|> "/member role " <|> "/mr #" <|> "/mr ") *> (MemberRole <$> displayName <* A.space <* optional (A.char '@') <*> displayName <*> memberRole),
("/remove #" <|> "/remove " <|> "/rm #" <|> "/rm ") *> (RemoveMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
("/leave #" <|> "/leave " <|> "/l #" <|> "/l ") *> (LeaveGroup <$> displayName),
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName),
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName),
("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName),
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName),
"/clear #" *> (ClearGroup <$> displayName),
("/clear @" <|> "/clear ") *> (ClearContact <$> displayName),
("/members #" <|> "/members " <|> "/ms #" <|> "/ms ") *> (ListMembers <$> displayName),
"/clear " *> char_ '@' *> (ClearContact <$> displayName),
("/members " <|> "/ms ") *> char_ '#' *> (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),
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile),
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName),
"/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> (jsonP <|> textP))),
"/_create link #" *> (APICreateGroupLink <$> A.decimal),
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
"/_get link #" *> (APIGetGroupLink <$> A.decimal),
@@ -3394,7 +3481,7 @@ chatCommandP =
"/delete link #" *> (DeleteGroupLink <$> displayName),
"/show link #" *> (ShowGroupLink <$> displayName),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> A.takeByteString),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* optional (A.char '@') <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString),
("/contacts" <|> "/cs") $> ListContacts,
("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
("/connect" <|> "/c") $> AddContact,
@@ -3418,8 +3505,8 @@ chatCommandP =
("/delete_address" <|> "/da") $> DeleteMyAddress,
("/show_address" <|> "/sa") $> ShowMyAddress,
"/auto_accept " *> (AddressAutoAccept <$> autoAcceptP),
("/accept @" <|> "/accept " <|> "/ac @" <|> "/ac ") *> (AcceptContact <$> displayName),
("/reject @" <|> "/reject " <|> "/rc @" <|> "/rc ") *> (RejectContact <$> displayName),
("/accept " <|> "/ac ") *> char_ '@' *> (AcceptContact <$> displayName),
("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayName),
("/markdown" <|> "/m") $> ChatHelp HSMarkdown,
("/welcome" <|> "/w") $> Welcome,
"/profile_image " *> (UpdateProfileImage . Just . ImageData <$> imageP),
@@ -3468,7 +3555,7 @@ chatCommandP =
gName <- displayName
fullName <- fullNameP gName
let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just GroupPreference {enable = FEOn}}
pure GroupProfile {displayName = gName, fullName, image = Nothing, groupPreferences}
pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences}
fullNameP name = do
n <- (A.space *> A.takeByteString) <|> pure ""
pure $ if B.null n then name else safeDecodeUtf8 n
@@ -3506,6 +3593,7 @@ chatCommandP =
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
(pure Nothing)
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
char_ = optional . A.char
adminContactReq :: ConnReqContact
adminContactReq =
+15 -1
View File
@@ -200,11 +200,19 @@ data ChatCommand
| APIGroupMemberInfo GroupId GroupMemberId
| APISwitchContact ContactId
| APISwitchGroupMember GroupId GroupMemberId
| APIGetContactCode ContactId
| APIGetGroupMemberCode GroupId GroupMemberId
| APIVerifyContact ContactId (Maybe Text)
| APIVerifyGroupMember GroupId GroupMemberId (Maybe Text)
| ShowMessages ChatName Bool
| ContactInfo ContactName
| GroupMemberInfo GroupName ContactName
| SwitchContact ContactName
| SwitchGroupMember GroupName ContactName
| GetContactCode ContactName
| GetGroupMemberCode GroupName ContactName
| VerifyContact ContactName (Maybe Text)
| VerifyGroupMember GroupName ContactName (Maybe Text)
| ChatHelp HelpSection
| Welcome
| AddContact
@@ -234,7 +242,9 @@ data ChatCommand
| ClearGroup GroupName
| ListMembers GroupName
| ListGroups
| UpdateGroupProfile GroupName GroupProfile
| UpdateGroupNames GroupName GroupProfile
| ShowGroupProfile GroupName
| UpdateGroupDescription GroupName (Maybe Text)
| CreateGroupLink GroupName
| DeleteGroupLink GroupName
| ShowGroupLink GroupName
@@ -276,6 +286,9 @@ data ChatResponse
| CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
| CRContactSwitch {contact :: Contact, switchProgress :: SwitchProgress}
| CRGroupMemberSwitch {groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress}
| CRContactCode {contact :: Contact, connectionCode :: Text}
| CRGroupMemberCode {groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
| CRConnectionVerified {verified :: Bool, expectedCode :: Text}
| CRNewChatItem {chatItem :: AChatItem}
| CRChatItemStatusUpdated {chatItem :: AChatItem}
| CRChatItemUpdated {chatItem :: AChatItem}
@@ -357,6 +370,7 @@ data ChatResponse
| CRGroupRemoved {groupInfo :: GroupInfo}
| CRGroupDeleted {groupInfo :: GroupInfo, member :: GroupMember}
| CRGroupUpdated {fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
| CRGroupProfile {groupInfo :: GroupInfo}
| CRGroupLinkCreated {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
| CRGroupLink {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
| CRGroupLinkDeleted {groupInfo :: GroupInfo}
+3
View File
@@ -123,9 +123,12 @@ groupsHelpInfo =
indent <> highlight "/leave <group> " <> " - leave group",
indent <> highlight "/delete <group> " <> " - delete group",
indent <> highlight "/members <group> " <> " - list group members",
indent <> highlight "/gp <group> " <> " - view group profile",
indent <> highlight "/gp <group> <new_name> [<full_name>] " <> " - update group profile",
indent <> highlight "/group_descr <group> [<descr>] " <> " - update/remove group description",
indent <> highlight "/groups " <> " - list groups",
indent <> highlight "#<group> <message> " <> " - send message to group",
indent <> highlight "/create link #<group> " <> " - create public group link",
"",
"The commands may be abbreviated: " <> listHighlight ["/g", "/a", "/j", "/rm", "/l", "/d", "/ms", "/gs"]
]
@@ -0,0 +1,13 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20221209_verified_connection where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20221209_verified_connection :: Query
m20221209_verified_connection =
[sql|
ALTER TABLE connections ADD COLUMN security_code TEXT NULL;
ALTER TABLE connections ADD COLUMN security_code_verified_at TEXT NULL;
|]
@@ -0,0 +1,16 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20221210_idxs where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20221210_idxs :: Query
m20221210_idxs =
[sql|
CREATE INDEX idx_messages_connection_id ON messages(connection_id);
CREATE INDEX idx_chat_items_group_member_id ON chat_items(group_member_id);
CREATE INDEX idx_chat_items_contact_id ON chat_items(contact_id);
|]
@@ -0,0 +1,12 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20221211_group_description where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20221211_group_description :: Query
m20221211_group_description =
[sql|
ALTER TABLE group_profiles ADD COLUMN description TEXT NULL;
|]
+7 -1
View File
@@ -116,7 +116,8 @@ CREATE TABLE group_profiles(
updated_at TEXT CHECK(updated_at NOT NULL),
image TEXT,
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE,
preferences TEXT
preferences TEXT,
description TEXT NULL
);
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY, -- local group ID
@@ -258,6 +259,8 @@ CREATE TABLE connections(
local_alias DEFAULT '' CHECK(local_alias NOT NULL),
via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL),
group_link_id BLOB,
security_code TEXT NULL,
security_code_verified_at TEXT NULL,
FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
@@ -450,3 +453,6 @@ CREATE UNIQUE INDEX idx_user_contact_links_group_id ON user_contact_links(
CREATE UNIQUE INDEX idx_snd_files_last_inline_msg_delivery_id ON snd_files(
last_inline_msg_delivery_id
);
CREATE INDEX idx_messages_connection_id ON messages(connection_id);
CREATE INDEX idx_chat_items_group_member_id ON chat_items(group_member_id);
CREATE INDEX idx_chat_items_contact_id ON chat_items(contact_id);
+106 -62
View File
@@ -35,6 +35,7 @@ module Simplex.Chat.Store
createDirectContact,
deleteContactConnectionsAndFiles,
deleteContact,
deleteContactWithoutGroups,
getContactByName,
getContact,
getContactIdByName,
@@ -46,7 +47,9 @@ module Simplex.Chat.Store
updateContactUsed,
updateContactUnreadChat,
updateGroupUnreadChat,
setConnectionVerified,
getUserContacts,
getUserContactProfiles,
createUserContactLink,
getUserAddressConnections,
getUserContactLinks,
@@ -92,6 +95,7 @@ module Simplex.Chat.Store
getUserGroups,
getUserGroupDetails,
getContactGroupPreferences,
checkContactHasGroups,
getGroupInvitation,
createNewContactMember,
createNewContactMemberAsync,
@@ -301,6 +305,9 @@ import Simplex.Chat.Migrations.M20221112_server_password
import Simplex.Chat.Migrations.M20221115_server_cfg
import Simplex.Chat.Migrations.M20221129_delete_group_feature_items
import Simplex.Chat.Migrations.M20221130_delete_item_deleted
import Simplex.Chat.Migrations.M20221209_verified_connection
import Simplex.Chat.Migrations.M20221210_idxs
import Simplex.Chat.Migrations.M20221211_group_description
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..))
@@ -351,7 +358,10 @@ schemaMigrations =
("20221112_server_password", m20221112_server_password),
("20221115_server_cfg", m20221115_server_cfg),
("20221129_delete_group_feature_items", m20221129_delete_group_feature_items),
("20221130_delete_item_deleted", m20221130_delete_item_deleted)
("20221130_delete_item_deleted", m20221130_delete_item_deleted),
("20221209_verified_connection", m20221209_verified_connection),
("20221210_idxs", m20221210_idxs),
("20221211_group_description", m20221211_group_description)
]
-- | The list of migrations in ascending order by date
@@ -462,7 +472,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
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.group_link_id, 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
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_id
@@ -537,7 +547,7 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
)
connId <- insertedRowId db
pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs}
pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing}
where
ent ct = if connType == ct then entityId else Nothing
@@ -595,6 +605,15 @@ deleteContact db user@User {userId} Contact {contactId, localDisplayName, active
DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId)
forM_ customUserProfileId $ \profileId -> deleteUnusedIncognitoProfileById_ db user profileId
-- should only be used if contact is not member of any groups
deleteContactWithoutGroups :: DB.Connection -> User -> Contact -> IO ()
deleteContactWithoutGroups 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)
deleteContactProfile_ db userId contactId
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
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
@@ -710,6 +729,11 @@ updateGroupUnreadChat db User {userId} GroupInfo {groupId} unreadChat = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (unreadChat, updatedAt, userId, groupId)
setConnectionVerified :: DB.Connection -> User -> Int64 -> Maybe Text -> IO ()
setConnectionVerified db User {userId} connId code = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId)
updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO ()
updateContactProfile_ db userId profileId profile = do
currentTs <- getCurrentTime
@@ -770,6 +794,22 @@ getUserContacts db user@User {userId} = do
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getContact db user) contactIds
-- only used in tests
getUserContactProfiles :: DB.Connection -> User -> IO [Profile]
getUserContactProfiles db User {userId} =
map toContactProfile
<$> DB.query
db
[sql|
SELECT display_name, full_name, image, preferences
FROM contact_profiles
WHERE user_id = ?
|]
(Only userId)
where
toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe Preferences) -> (Profile)
toContactProfile (displayName, fullName, image, preferences) = Profile {displayName, fullName, image, preferences}
createUserContactLink :: DB.Connection -> UserId -> ConnId -> ConnReqContact -> ExceptT StoreError IO ()
createUserContactLink db userId agentConnId cReq =
checkConstraint SEDuplicateContactLink . liftIO $ do
@@ -793,7 +833,7 @@ getUserAddressConnections db User {userId} = do
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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
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, c.security_code, c.security_code_verified_at
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
@@ -807,7 +847,7 @@ getUserContactLinks db User {userId} =
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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,
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, c.security_code, c.security_code_verified_at,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
@@ -940,7 +980,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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
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, c.security_code, c.security_code_verified_at
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
@@ -1043,7 +1083,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
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.group_link_id, 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
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@@ -1238,7 +1278,7 @@ getContactConnections db userId Contact {contactId} =
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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
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, c.security_code, c.security_code_verified_at
FROM connections c
JOIN contacts ct ON ct.contact_id = c.contact_id
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
@@ -1249,14 +1289,15 @@ getContactConnections db userId Contact {contactId} =
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. Only UTCTime
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime)
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. Only (Maybe UTCTime)
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime)
toConnection :: ConnectionRow -> Connection
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) =
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_)) =
let entityId = entityId_ connType
in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, createdAt}
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, createdAt}
where
entityId_ :: ConnType -> Maybe Int64
entityId_ ConnContact = contactId
@@ -1266,8 +1307,8 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
entityId_ ConnUserContact = userContactLinkId
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) =
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt)
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_)) =
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_))
toMaybeConnection _ = Nothing
getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact]
@@ -1438,7 +1479,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at
FROM connections
WHERE user_id = ? AND agent_conn_id = ?
|]
@@ -1470,7 +1511,7 @@ 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, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, 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,
@@ -1536,7 +1577,7 @@ getConnectionById db User {userId} connId = ExceptT $ do
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at
FROM connections
WHERE user_id = ? AND connection_id = ?
|]
@@ -1571,7 +1612,7 @@ 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, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, 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,
@@ -1581,7 +1622,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
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.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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
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, c.security_code, c.security_code_verified_at
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
@@ -1611,15 +1652,15 @@ 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, groupPreferences} = groupProfile
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
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, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
@@ -1655,7 +1696,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation_ = do
let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
ExceptT $
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
@@ -1663,8 +1704,8 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
groupId <- liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
@@ -1759,7 +1800,9 @@ deleteGroupItemsAndMembers db user@User {userId} GroupInfo {groupId} members = d
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId)
void $ runExceptT cleanupHostGroupLinkConn_ -- to allow repeat connection via the same group link if one was used
DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId)
forM_ members $ \m -> cleanupMemberContactAndProfile_ db user m
forM_ members $ \m@GroupMember {memberProfile = LocalProfile {profileId}} -> do
cleanupMemberProfileAndName_ db user m
when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user profileId
where
cleanupHostGroupLinkConn_ = do
hostId <- getHostMemberId_ db user groupId
@@ -1777,10 +1820,11 @@ deleteGroupItemsAndMembers db user@User {userId} GroupInfo {groupId} members = d
(userId, userId, hostId)
deleteGroup :: DB.Connection -> User -> GroupInfo -> IO ()
deleteGroup db User {userId} GroupInfo {groupId, localDisplayName} = do
deleteGroup db user@User {userId} GroupInfo {groupId, localDisplayName, membership = membership@GroupMember {memberProfile = LocalProfile {profileId}}} = do
deleteGroupProfile_ db userId groupId
DB.execute db "DELETE FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId)
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
when (memberIncognito membership) $ deleteUnusedIncognitoProfileById_ db user profileId
deleteGroupProfile_ :: DB.Connection -> UserId -> GroupId -> IO ()
deleteGroupProfile_ db userId groupId =
@@ -1807,7 +1851,7 @@ 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, gp.preferences, g.created_at, g.updated_at,
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, 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, pu.preferences
FROM groups g
@@ -1832,19 +1876,24 @@ getContactGroupPreferences db User {userId} Contact {contactId} = do
|]
(userId, contactId)
checkContactHasGroups :: DB.Connection -> User -> Contact -> IO (Maybe GroupId)
checkContactHasGroups db User {userId} Contact {contactId} =
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId)
getGroupInfoByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupInfo
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, Maybe GroupPreferences, UTCTime, UTCTime) :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe 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_, groupPreferences, createdAt, updatedAt) :. userMemberRow) =
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, groupPreferences, createdAt, updatedAt) :. userMemberRow) =
let membership = toGroupMember userContactId userMemberRow
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
fullGroupPreferences = mergeGroupPreferences groupPreferences
in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image, groupPreferences}, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
getGroupMember db user@User {userId} groupId groupMemberId =
@@ -1856,7 +1905,7 @@ getGroupMember db user@User {userId} groupId groupMemberId =
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.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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
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, c.security_code, c.security_code_verified_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 = (
@@ -1878,7 +1927,7 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do
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.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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
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, c.security_code, c.security_code_verified_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 = (
@@ -1900,7 +1949,7 @@ getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {gro
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.group_link_id, 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
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, c.security_code, c.security_code_verified_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 = (
@@ -2025,7 +2074,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
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.group_link_id, 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
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
JOIN connections c ON c.connection_id = (
@@ -2130,27 +2179,22 @@ 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} = do
deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId, memberProfile = LocalProfile {profileId}} = 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)
cleanupMemberContactAndProfile_ db user m
cleanupMemberProfileAndName_ db user m
when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user profileId
-- 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 user 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
cleanupMemberProfileAndName_ :: DB.Connection -> User -> GroupMember -> IO ()
cleanupMemberProfileAndName_ db User {userId} GroupMember {groupMemberId, memberContactId, memberContactProfileId, localDisplayName} =
-- check record has no memberContactId (contact_id) - it means contact has been deleted and doesn't use profile & ldn
when (isNothing memberContactId) $ do
-- check other group member records don't use profile & ldn
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)
when (isNothing 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)
deleteGroupMemberConnection :: DB.Connection -> User -> GroupMember -> IO ()
deleteGroupMemberConnection db User {userId} GroupMember {groupMemberId} =
@@ -2325,7 +2369,7 @@ 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, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, 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,
@@ -2335,7 +2379,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
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.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, 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
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, c.security_code, c.security_code_verified_at
FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -2367,7 +2411,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
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, 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.group_link_id, 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
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, c.security_code, c.security_code_verified_at
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
JOIN connections c ON c.connection_id = (
@@ -3246,7 +3290,7 @@ getDirectChatPreviews_ db user@User {userId} = do
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.group_link_id, 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,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at,
-- ChatStats
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
-- ChatItem
@@ -3307,7 +3351,7 @@ 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, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, 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,
@@ -3568,7 +3612,7 @@ getContact db user@User {userId} contactId =
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.group_link_id, 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
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@@ -3673,7 +3717,7 @@ 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, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, 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,
@@ -3687,7 +3731,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, groupPreferences}
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
updateGroupProfile_ currentTs $> (g :: GroupInfo) {groupProfile = p', fullGroupPreferences}
@@ -3704,14 +3748,14 @@ updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, grou
db
[sql|
UPDATE group_profiles
SET display_name = ?, full_name = ?, image = ?, preferences = ?, updated_at = ?
SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
FROM groups
WHERE user_id = ? AND group_id = ?
)
|]
(newName, fullName, image, groupPreferences, currentTs, userId, groupId)
(newName, fullName, description, image, groupPreferences, currentTs, userId, groupId)
updateGroup_ ldn currentTs = do
DB.execute
db
+27
View File
@@ -20,6 +20,7 @@
module Simplex.Chat.Types where
import Control.Applicative ((<|>))
import Crypto.Number.Serialize (os2ip)
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
@@ -126,6 +127,9 @@ directContact Contact {contactUsed, activeConn = Connection {connLevel, viaGroup
anyDirectContact :: Contact -> Bool
anyDirectContact Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed
contactSecurityCode :: Contact -> Maybe SecurityCode
contactSecurityCode Contact {activeConn} = connectionCode activeConn
data ContactRef = ContactRef
{ contactId :: ContactId,
localDisplayName :: ContactName
@@ -720,6 +724,7 @@ fromLocalProfile LocalProfile {displayName, fullName, image, preferences} =
data GroupProfile = GroupProfile
{ displayName :: GroupName,
fullName :: Text,
description :: Maybe Text,
image :: Maybe ImageData,
groupPreferences :: Maybe GroupPreferences
}
@@ -866,6 +871,9 @@ groupMemberId' GroupMember {groupMemberId} = groupMemberId
memberIncognito :: GroupMember -> Bool
memberIncognito GroupMember {memberProfile, memberContactProfileId} = localProfileId memberProfile /= memberContactProfileId
memberSecurityCode :: GroupMember -> Maybe SecurityCode
memberSecurityCode GroupMember {activeConn} = connectionCode =<< activeConn
data NewGroupMember = NewGroupMember
{ memInfo :: MemberInfo,
memCategory :: GroupMemberCategory,
@@ -1302,10 +1310,29 @@ data Connection = Connection
connStatus :: ConnStatus,
localAlias :: Text,
entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID
connectionCode :: Maybe SecurityCode,
createdAt :: UTCTime
}
deriving (Eq, Show, Generic)
data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime}
deriving (Eq, Show, Generic)
instance ToJSON SecurityCode where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
verificationCode :: ByteString -> Text
verificationCode = T.pack . unwords . chunks 5 . show . os2ip
where
chunks _ [] = []
chunks n xs = let (h, t) = splitAt n xs in h : chunks n t
sameVerificationCode :: Text -> Text -> Bool
sameVerificationCode c1 c2 = noSpaces c1 == noSpaces c2
where
noSpaces = T.filter (/= ' ')
aConnId :: Connection -> ConnId
aConnId Connection {agentConnId = AgentConnId cId} = cId
+53 -18
View File
@@ -74,6 +74,9 @@ responseToView user_ testView ts = \case
CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats
CRContactSwitch ct progress -> viewContactSwitch ct progress
CRGroupMemberSwitch g m progress -> viewGroupMemberSwitch g m progress
CRConnectionVerified verified code -> [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRContactCode ct code -> viewContactCode ct code testView
CRGroupMemberCode g m code -> viewGroupMemberCode g m code testView
CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False ts
CRLastMessages chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts) chatItems
CRChatItemStatusUpdated _ -> []
@@ -181,6 +184,7 @@ responseToView user_ testView ts = \case
CRGroupRemoved g -> [ttyFullGroup g <> ": you are no longer a member or group deleted"]
CRGroupDeleted g m -> [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"]
CRGroupUpdated g g' m -> viewGroupUpdated g g' m
CRGroupProfile g -> viewGroupProfile g
CRGroupLinkCreated g cReq -> groupLink_ "Group link is created!" g cReq
CRGroupLink g cReq -> groupLink_ "Group link:" g cReq
CRGroupLinkDeleted g -> viewGroupLinkDeleted g
@@ -485,9 +489,9 @@ viewReceivedContactRequest c Profile {fullName} =
]
viewGroupCreated :: GroupInfo -> [StyledString]
viewGroupCreated g@GroupInfo {localDisplayName} =
viewGroupCreated g@GroupInfo {localDisplayName = n} =
[ "group " <> ttyFullGroup g <> " is created",
"use " <> highlight ("/a " <> localDisplayName <> " <name>") <> " to add members"
"to add members use " <> highlight ("/a " <> n <> " <name>") <> " or " <> highlight ("/create link #" <> n)
]
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
@@ -497,8 +501,8 @@ viewCannotResendInvitation GroupInfo {localDisplayName = gn} c =
]
viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString]
viewDirectMessagesProhibited MDSnd c = [ "direct messages to indirect contact " <> ttyContact' c <> " are prohibited"]
viewDirectMessagesProhibited MDRcv c = [ "received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"]
viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"]
viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"]
viewUserJoinedGroup :: GroupInfo -> [StyledString]
viewUserJoinedGroup g@GroupInfo {membership = membership@GroupMember {memberProfile}} =
@@ -578,7 +582,7 @@ viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView
where
message =
[ ttyFullContact ct <> ": contact is connected, your incognito profile for this contact is " <> incognitoProfile' profile,
"use " <> highlight ("/info " <> localDisplayName) <> " to print out this incognito profile again"
"use " <> highlight ("/i " <> localDisplayName) <> " to print out this incognito profile again"
]
Nothing ->
[ttyFullContact ct <> ": contact is connected"]
@@ -681,21 +685,27 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
]
viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo Contact {contactId, profile = LocalProfile {localAlias}} stats incognitoProfile =
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias}} stats incognitoProfile =
["contact ID: " <> sShow contactId] <> viewConnectionStats stats
<> maybe
["you've shared main profile with this contact"]
(\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p])
incognitoProfile
<> if localAlias /= "" then ["alias: " <> plain localAlias] else ["alias not set"]
<> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (contactSecurityCode ct)]
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString]
viewGroupMemberInfo GroupInfo {groupId} GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats =
viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats =
[ "group ID: " <> sShow groupId,
"member ID: " <> sShow groupMemberId
]
<> maybe ["member not connected"] viewConnectionStats stats
<> if localAlias /= "" then ["alias: " <> plain localAlias] else ["no alias for contact"]
<> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (memberSecurityCode m) | isJust stats]
viewConnectionVerified :: Maybe SecurityCode -> StyledString
viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time?
viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code"
viewConnectionStats :: ConnectionStats -> [StyledString]
viewConnectionStats ConnectionStats {rcvServers, sndServers} =
@@ -720,6 +730,17 @@ 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"]
viewContactCode :: Contact -> Text -> Bool -> [StyledString]
viewContactCode ct@Contact {localDisplayName = c} = viewSecurityCode (ttyContact' ct) ("/verify " <> c <> " <code from your contact>")
viewGroupMemberCode :: GroupInfo -> GroupMember -> Text -> Bool -> [StyledString]
viewGroupMemberCode g m@GroupMember {localDisplayName = n} = viewSecurityCode (ttyGroup' g <> " " <> ttyMember m) ("/verify #" <> groupName' g <> " " <> n <> " <code from your contact>")
viewSecurityCode :: StyledString -> Text -> Text -> Bool -> [StyledString]
viewSecurityCode name cmd code testView
| testView = [plain code]
| otherwise = [name <> " security code:", plain code, "pass this code to your contact and use " <> highlight cmd <> " to verify"]
viewSwitchPhase :: SwitchPhase -> StyledString
viewSwitchPhase SPCompleted = "changed address"
viewSwitchPhase phase = plain (strEncode phase) <> " changing address"
@@ -789,8 +810,8 @@ viewCountactUserPref = \case
viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> [StyledString]
viewGroupUpdated
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, image, groupPreferences = gps}}
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', image = image', groupPreferences = gps'}}
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, description, image, groupPreferences = gps}}
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', description = description', image = image', groupPreferences = gps'}}
m = do
let update = groupProfileUpdated <> groupPrefsUpdated
if null update
@@ -798,21 +819,35 @@ viewGroupUpdated
else memberUpdated <> update
where
memberUpdated = maybe [] (\m' -> [ttyMember m' <> " updated group " <> ttyGroup n <> ":"]) m
groupProfileUpdated
| n == n' && fullName == fullName' && image == image' = []
| n == n' && fullName == fullName' = ["profile image " <> (if isNothing image' then "removed" else "updated")]
| n == n' = ["full name " <> if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName']
| otherwise = ["changed to " <> ttyFullGroup g']
groupProfileUpdated =
["changed to " <> ttyFullGroup g' | n /= n']
<> ["full name " <> if T.null fullName' || fullName' == n' then "removed" else "changed to: " <> plain fullName' | n == n' && fullName /= fullName']
<> ["profile image " <> maybe "removed" (const "updated") image' | image /= image']
<> (if description == description' then [] else maybe ["description removed"] ((bold' "description changed to:" :) . map plain . T.lines) description')
groupPrefsUpdated
| null prefs = []
| otherwise = "updated group preferences:" : prefs
| otherwise = bold' "updated group preferences:" : prefs
where
prefs = mapMaybe viewPref allGroupFeatures
viewPref pt
| pref gps == pref gps' = Nothing
| otherwise = Just $ plain (groupFeatureToText pt) <> " enabled: " <> plain (groupPrefToText $ pref gps')
where
pref pss = getGroupPreference pt $ mergeGroupPreferences pss
pref = getGroupPreference pt . mergeGroupPreferences
viewGroupProfile :: GroupInfo -> [StyledString]
viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {description, image, groupPreferences = gps}} =
[ttyFullGroup g]
<> maybe [] (const ["has profile image"]) image
<> maybe [] ((bold' "description:" :) . map plain . T.lines) description
<> (bold' "group preferences:" : map viewPref allGroupFeatures)
where
viewPref pt = plain (groupFeatureToText pt) <> " enabled: " <> plain (groupPrefToText $ pref gps)
where
pref = getGroupPreference pt . mergeGroupPreferences
bold' :: String -> StyledString
bold' = styled Bold
viewContactAliasUpdated :: Contact -> [StyledString]
viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}}
+1 -1
View File
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
commit: e4842f4f47fb60ef8843dbce6fd43dec96f157d2
commit: fb21d9836e07706c7498baa967f932cb11b818e5
# - ../direct-sqlcipher
- github: simplex-chat/direct-sqlcipher
commit: 34309410eb2069b029b8fc1872deb1e0db123294
+1 -1
View File
@@ -215,7 +215,7 @@ getTermLine :: TestCC -> IO String
getTermLine cc =
5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case
Just s -> do
-- uncomment code below to echo virtual terminal
-- uncomment 2 lines below to echo virtual terminal
-- name <- userName cc
-- putStrLn $ name <> ": " <> s
pure s
+583 -51
View File
@@ -24,7 +24,10 @@ 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.Store (getUserContactProfiles)
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Store.SQLite (withTransaction)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (unlessM)
import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist)
@@ -50,6 +53,7 @@ chatTests :: Spec
chatTests = do
describe "direct messages" $ do
describe "add contact and send/receive message" testAddContact
it "deleting contact deletes profile" testDeleteContactDeletesProfile
it "direct message quoted replies" testDirectMessageQuotedReply
it "direct message update" testDirectMessageUpdate
it "direct message delete" testDirectMessageDelete
@@ -71,6 +75,8 @@ chatTests = do
it "group message delete" testGroupMessageDelete
it "update group profile" testUpdateGroupProfile
it "update member role" testUpdateMemberRole
it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts
it "group description is shown as the first message to new members" testGroupDescription
describe "async group connections" $ do
xit "create and join group when clients go offline" testGroupAsync
describe "user profiles" $ do
@@ -116,6 +122,15 @@ chatTests = do
it "join group incognito" testJoinGroupIncognito
it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito
it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito
it "deleting contact first, group second deletes incognito profile" testDeleteContactThenGroupDeletesIncognitoProfile
it "deleting group first, contact second deletes incognito profile" testDeleteGroupThenContactDeletesIncognitoProfile
describe "group links" $ do
it "create group link, join via group link" testGroupLink
it "delete group, re-join via same link" testGroupLinkDeleteGroupRejoin
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 "unused host contact is deleted after all groups with it are deleted" testGroupLinkUnusedHostContactDeleted
it "leaving groups with unused host contacts deletes incognito profiles" testGroupLinkIncognitoUnusedHostContactsDeleted
describe "contact aliases" $ do
it "set contact alias" testSetAlias
it "set connection alias" testSetConnectionAlias
@@ -152,14 +167,16 @@ chatTests = do
it "mute/unmute group" testMuteGroup
describe "chat item expiration" $ do
it "set chat item TTL" testSetChatItemTTL
describe "group links" $ do
it "create group link, join via group link" testGroupLink
it "delete group, re-join via same link" testGroupLinkDeleteGroupRejoin
it "sending message to contact created via group link marks it used" testGroupLinkContactUsed
it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership
describe "queue rotation" $ do
it "switch contact to a different queue" testSwitchContact
it "switch group member to a different queue" testSwitchGroupMember
describe "connection verification code" $ do
it "verificationCode function converts ByteString to series of digits" $
verificationCode (C.sha256Hash "abcd") `shouldBe` "61889 38426 63934 09576 96390 79389 84124 85253 63658 69469 70853 37788 95900 68296 20156 25"
it "sameVerificationCode function should ignore spaces" $
sameVerificationCode "123 456 789" "12345 6789" `shouldBe` True
it "mark contact verified" testMarkContactVerified
it "mark group member verified" testMarkGroupMemberVerified
versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec
versionTestMatrix2 runTest = do
@@ -260,7 +277,9 @@ testAddContact = versionTestMatrix2 runTestAddContact
alice ##> "@bob_1 hey"
alice <## "no contact bob_1"
alice @@@ [("@bob", "how are you?")]
alice `hasContactProfiles` ["alice", "bob"]
bob @@@ [("@alice_1", "hi"), ("@alice", "how are you?")]
bob `hasContactProfiles` ["alice", "alice", "bob"]
-- test clearing chat
alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
alice #$> ("/_get chat @2 count=100", chat, [])
@@ -292,6 +311,25 @@ testAddContact = versionTestMatrix2 runTestAddContact
alice #$> ("/_read chat @2", id, "ok")
bob #$> ("/_read chat @2", id, "ok")
testDeleteContactDeletesProfile :: IO ()
testDeleteContactDeletesProfile =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice <##> bob
-- alice deletes contact, profile is deleted
alice ##> "/d bob"
alice <## "bob: contact is deleted"
alice ##> "/cs"
(alice </)
alice `hasContactProfiles` ["alice"]
-- bob deletes contact, profile is deleted
bob ##> "/d alice"
bob <## "alice: contact is deleted"
bob ##> "/cs"
(bob </)
bob `hasContactProfiles` ["bob"]
testDirectMessageQuotedReply :: IO ()
testDirectMessageQuotedReply =
testChat2 aliceProfile bobProfile $
@@ -461,7 +499,7 @@ testGroupShared alice bob cath checkMessages = do
connectUsers alice cath
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -596,7 +634,7 @@ testGroup2 =
connectUsers alice dan
alice ##> "/g club"
alice <## "group #club is created"
alice <## "use /a club <name> to add members"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
@@ -814,10 +852,10 @@ testGroupSameName =
\alice _ -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/g team"
alice <## "group #team_1 (team) is created"
alice <## "use /a team_1 <name> to add members"
alice <## "to add members use /a team_1 <name> or /create link #team_1"
testGroupDeleteWhenInvited :: IO ()
testGroupDeleteWhenInvited =
@@ -826,7 +864,7 @@ testGroupDeleteWhenInvited =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -853,7 +891,7 @@ testGroupReAddInvited =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -888,7 +926,7 @@ testGroupReAddInvitedChangeRole =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -928,7 +966,7 @@ testGroupDeleteInvitedContact =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -959,7 +997,7 @@ testDeleteGroupMemberProfileKept =
-- group 1
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -978,7 +1016,7 @@ testDeleteGroupMemberProfileKept =
-- group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "use /a club <name> to add members"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
@@ -1071,7 +1109,7 @@ testGroupList =
createGroup2 "team" alice bob
alice ##> "/g tennis"
alice <## "group #tennis is created"
alice <## "use /a tennis <name> to add members"
alice <## "to add members use /a tennis <name> or /create link #tennis"
alice ##> "/a tennis bob"
concurrentlyN_
[ alice <## "invitation to join the group #tennis sent to bob",
@@ -1340,7 +1378,7 @@ testUpdateMemberRole =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
addMember "team" alice bob GRAdmin
alice ##> "/mr team bob member"
alice <## "#team: you changed the role of bob from admin to member"
@@ -1379,6 +1417,154 @@ testUpdateMemberRole =
alice ##> "/d #team"
alice <## "you have insufficient permissions for this group command"
testGroupDeleteUnusedContacts :: IO ()
testGroupDeleteUnusedContacts =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
-- create group 1
createGroup3 "team" alice bob cath
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
do
bob <## "#club: alice invites you to join the group as admin"
bob <## "use /j club to accept"
]
bob ##> "/j club"
concurrently_
(alice <## "#club: bob joined the group")
(bob <## "#club: you joined the group")
alice ##> "/a club cath"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to cath",
do
cath <## "#club: alice invites you to join the group as admin"
cath <## "use /j club to accept"
]
cath ##> "/j club"
concurrentlyN_
[ alice <## "#club: cath joined the group",
do
cath <## "#club: you joined the group"
cath <## "#club: member bob_1 (Bob) is connected"
cath <## "contact bob_1 is merged into bob"
cath <## "use @bob <message> to send messages",
do
bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)"
bob <## "#club: new member cath_1 is connected"
bob <## "contact cath_1 is merged into cath"
bob <## "use @cath <message> to send messages"
]
-- list contacts
bob ##> "/cs"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
cath ##> "/cs"
cath <## "alice (Alice)"
cath <## "bob (Bob)"
-- delete group 1, contacts and profiles are kept
deleteGroup alice bob cath "team"
bob ##> "/cs"
bob <## "alice (Alice)"
bob <## "cath (Catherine)"
bob `hasContactProfiles` ["alice", "bob", "cath"]
cath ##> "/cs"
cath <## "alice (Alice)"
cath <## "bob (Bob)"
cath `hasContactProfiles` ["alice", "bob", "cath"]
-- delete group 2, unused contacts and profiles are deleted
deleteGroup alice bob cath "club"
bob ##> "/cs"
bob <## "alice (Alice)"
bob `hasContactProfiles` ["alice", "bob"]
cath ##> "/cs"
cath <## "alice (Alice)"
cath `hasContactProfiles` ["alice", "cath"]
where
deleteGroup alice bob cath group = do
alice ##> ("/d #" <> group)
concurrentlyN_
[ alice <## ("#" <> group <> ": you deleted the group"),
do
bob <## ("#" <> group <> ": alice deleted the group")
bob <## ("use /d #" <> group <> " to delete the local copy of the group"),
do
cath <## ("#" <> group <> ": alice deleted the group")
cath <## ("use /d #" <> group <> " to delete the local copy of the group")
]
bob ##> ("/d #" <> group)
bob <## ("#" <> group <> ": you deleted the group")
cath ##> ("/d #" <> group)
cath <## ("#" <> group <> ": you deleted the group")
testGroupDescription :: IO ()
testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
addMember "team" alice bob GRAdmin
bob ##> "/j team"
concurrentlyN_
[ alice <## "#team: bob joined the group",
bob <## "#team: you joined the group"
]
alice ##> "/group_profile team"
alice <## "#team"
groupInfo alice
alice ##> "/group_descr team Welcome to the team!"
alice <## "description changed to:"
alice <## "Welcome to the team!"
bob <## "alice updated group #team:"
bob <## "description changed to:"
bob <## "Welcome to the team!"
alice ##> "/group_profile team"
alice <## "#team"
alice <## "description:"
alice <## "Welcome to the team!"
groupInfo alice
connectUsers alice cath
addMember "team" alice cath GRMember
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
do
cath <## "#team: you joined the group"
cath <# "#team alice> Welcome to the team!"
cath <## "#team: member bob (Bob) is connected",
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
connectUsers bob dan
addMember "team" bob dan GRMember
dan ##> "/j team"
concurrentlyN_
[ bob <## "#team: dan joined the group",
do
dan <## "#team: you joined the group"
dan <# "#team bob> Welcome to the team!"
dan
<### [ "#team: member alice (Alice) is connected",
"#team: member cath (Catherine) is connected"
],
bobAddedDan alice,
bobAddedDan cath
]
where
groupInfo alice = do
alice <## "group preferences:"
alice <## "Direct messages enabled: on"
alice <## "Full deletion enabled: off"
alice <## "Voice messages enabled: on"
bobAddedDan cc = do
cc <## "#team: bob added dan (Daniel) to the group (connecting...)"
cc <## "#team: new member dan is connected"
testGroupAsync :: IO ()
testGroupAsync = withTmpFiles $ do
print (0 :: Integer)
@@ -1387,7 +1573,7 @@ testGroupAsync = withTmpFiles $ do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -2570,7 +2756,7 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $
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",
<### [ "use /i bob to print out this incognito profile again",
WithTime "i @bob hello!"
]
]
@@ -2589,10 +2775,10 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi
concurrentlyN_
[ do
bob <## (aliceIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /info " <> aliceIncognito <> " to print out this incognito profile again"),
bob <## ("use /i " <> aliceIncognito <> " to print out this incognito profile again"),
do
alice <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## ("use /info " <> bobIncognito <> " to print out this incognito profile again")
alice <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
]
-- after turning incognito mode off conversation is incognito
alice #$> ("/incognito off", id, "ok")
@@ -2630,6 +2816,28 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi
alice <## "Full deletion: off (you allow: no, contact allows: no)"
bob <## (aliceIncognito <> " updated preferences for you:")
bob <## "Full deletion: off (you allow: no, contact allows: no)"
-- list contacts
alice ##> "/cs"
alice
<### [ ConsoleString $ "i " <> bobIncognito,
"cath (Catherine)"
]
alice `hasContactProfiles` ["alice", T.pack aliceIncognito, T.pack bobIncognito, "cath"]
bob ##> "/cs"
bob <## ("i " <> aliceIncognito)
bob `hasContactProfiles` ["bob", T.pack aliceIncognito, T.pack bobIncognito]
-- alice deletes contact, incognito profile is deleted
alice ##> ("/d " <> bobIncognito)
alice <## (bobIncognito <> ": contact is deleted")
alice ##> "/cs"
alice <## "cath (Catherine)"
alice `hasContactProfiles` ["alice", "cath"]
-- bob deletes contact, incognito profile is deleted
bob ##> ("/d " <> aliceIncognito)
bob <## (aliceIncognito <> ": contact is deleted")
bob ##> "/cs"
(bob </)
bob `hasContactProfiles` ["bob"]
testConnectIncognitoContactAddress :: IO ()
testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
@@ -2649,7 +2857,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
concurrentlyN_
[ do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /info alice to print out this incognito profile again",
bob <## "use /i alice to print out this incognito profile again",
alice <## (bobIncognito <> ": contact is connected")
]
-- after turning incognito mode off conversation is incognito
@@ -2659,6 +2867,16 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
bob ?<# "alice> who are you?"
bob ?#> "@alice I'm Batman"
alice <# (bobIncognito <> "> I'm Batman")
-- list contacts
bob ##> "/cs"
bob <## "i alice (Alice)"
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
-- delete contact, incognito profile is deleted
bob ##> "/d alice"
bob <## "alice: contact is deleted"
bob ##> "/cs"
(bob </)
bob `hasContactProfiles` ["bob"]
testAcceptContactRequestIncognito :: IO ()
testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $
@@ -2675,7 +2893,7 @@ testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $
[ 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"
alice <## "use /i bob to print out this incognito profile again"
]
-- after turning incognito mode off conversation is incognito
alice #$> ("/incognito off", id, "ok")
@@ -2684,6 +2902,16 @@ testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $
bob <# (aliceIncognito <> "> my profile is totally inconspicuous")
bob #> ("@" <> aliceIncognito <> " I know!")
alice ?<# "bob> I know!"
-- list contacts
alice ##> "/cs"
alice <## "i bob (Bob)"
alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito]
-- delete contact, incognito profile is deleted
alice ##> "/d bob"
alice <## "bob: contact is deleted"
alice ##> "/cs"
(alice </)
alice `hasContactProfiles` ["alice"]
testJoinGroupIncognito :: IO ()
testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $
@@ -2704,13 +2932,13 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil
concurrentlyN_
[ do
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
cath <## "use /info alice to print out this incognito profile again",
cath <## "use /i alice to print out this incognito profile again",
alice <## (cathIncognito <> ": contact is connected")
]
-- alice creates group
alice ##> "/g secret_club"
alice <## "group #secret_club is created"
alice <## "use /a secret_club <name> to add members"
alice <## "to add members use /a secret_club <name> or /create link #secret_club"
-- alice invites bob
alice ##> "/a secret_club bob"
concurrentlyN_
@@ -2890,13 +3118,13 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
[ 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"
alice <## "use /i bob to print out this incognito profile again"
]
-- alice creates group non incognito
alice #$> ("/incognito off", id, "ok")
alice ##> "/g club"
alice <## "group #club is created"
alice <## "use /a club <name> to add members"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/a club bob"
alice <## "you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"
-- bob doesn't receive invitation
@@ -2920,7 +3148,7 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr
[ 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",
alice <## "use /i bob to print out this incognito profile again",
do
cath <## "alice (Alice): contact is connected"
]
@@ -2955,6 +3183,110 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr
cath <## "alice updated preferences for you:"
cath <## "Full deletion: off (you allow: default (no), contact allows: yes)"
testDeleteContactThenGroupDeletesIncognitoProfile :: IO ()
testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobProfile $
\alice bob -> do
-- bob connects incognito to alice
alice ##> "/c"
inv <- getInvitation alice
bob #$> ("/incognito on", id, "ok")
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
bobIncognito <- getTermLine bob
concurrentlyN_
[ alice <## (bobIncognito <> ": contact is connected"),
do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again"
]
-- bob joins group using incognito profile
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> ("/a team " <> bobIncognito)
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
do
bob <## "#team: alice invites you to join the group as admin"
bob <## ("use /j team to join incognito as " <> bobIncognito)
]
bob ##> "/j team"
concurrently_
(alice <## ("#team: " <> bobIncognito <> " joined the group"))
(bob <## ("#team: you joined the group incognito as " <> bobIncognito))
bob ##> "/cs"
bob <## "i alice (Alice)"
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
-- delete contact
bob ##> "/d alice"
bob <## "alice: contact is deleted"
bob ##> "/cs"
(bob </)
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
-- delete group
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## ("#team: " <> bobIncognito <> " left the group")
]
bob ##> "/d #team"
bob <## "#team: you deleted the group"
bob `hasContactProfiles` ["bob"]
testDeleteGroupThenContactDeletesIncognitoProfile :: IO ()
testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobProfile $
\alice bob -> do
-- bob connects incognito to alice
alice ##> "/c"
inv <- getInvitation alice
bob #$> ("/incognito on", id, "ok")
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
bobIncognito <- getTermLine bob
concurrentlyN_
[ alice <## (bobIncognito <> ": contact is connected"),
do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again"
]
-- bob joins group using incognito profile
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> ("/a team " <> bobIncognito)
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
do
bob <## "#team: alice invites you to join the group as admin"
bob <## ("use /j team to join incognito as " <> bobIncognito)
]
bob ##> "/j team"
concurrently_
(alice <## ("#team: " <> bobIncognito <> " joined the group"))
(bob <## ("#team: you joined the group incognito as " <> bobIncognito))
bob ##> "/cs"
bob <## "i alice (Alice)"
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
-- delete group
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## ("#team: " <> bobIncognito <> " left the group")
]
bob ##> "/d #team"
bob <## "#team: you deleted the group"
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
-- delete contact
bob ##> "/d alice"
bob <## "alice: contact is deleted"
bob ##> "/cs"
(bob </)
bob `hasContactProfiles` ["bob"]
testSetAlias :: IO ()
testSetAlias = testChat2 aliceProfile bobProfile $
\alice bob -> do
@@ -3182,19 +3514,19 @@ testProhibitDirectMessages =
addMember "team" cath dan GRMember
dan ##> "/j #team"
concurrentlyN_
[ cath <## ("#team: dan joined the group"),
[ cath <## "#team: dan joined the group",
do
dan <## ("#team: you joined the group")
dan <###
[ "#team: member alice (Alice) is connected",
"#team: member bob (Bob) is connected"
],
dan <## "#team: you joined the group"
dan
<### [ "#team: member alice (Alice) is connected",
"#team: member bob (Bob) is connected"
],
do
alice <## ("#team: cath added dan (Daniel) to the group (connecting...)")
alice <## ("#team: new member dan is connected"),
alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
alice <## "#team: new member dan is connected",
do
bob <## ("#team: cath added dan (Daniel) to the group (connecting...)")
bob <## ("#team: new member dan is connected")
bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
bob <## "#team: new member dan is connected"
]
alice ##> "@dan hi"
alice <## "direct messages to indirect contact dan are prohibited"
@@ -3241,7 +3573,7 @@ testTestSMPServerConnection =
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001"
alice <## "SMP server test passed"
alice ##> "/smp test smp://LcJU@localhost:5001"
alice <## ("SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK")
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK"
alice <## "Possibly, certificate fingerprint in server address is incorrect"
testAsyncInitiatingOffline :: IO ()
@@ -3834,7 +4166,7 @@ testGroupLink =
\alice bob cath -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/show link #team"
alice <## "no group link, to create: /create link #team"
alice ##> "/create link #team"
@@ -3864,9 +4196,6 @@ testGroupLink =
-- 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", "connected")]
alice <##> bob
alice @@@ [("@bob", "hey"), ("#team", "connected")]
@@ -3937,7 +4266,7 @@ testGroupLinkDeleteGroupRejoin =
\alice bob -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
bob ##> ("/c " <> gLink)
@@ -3993,7 +4322,7 @@ testGroupLinkContactUsed =
\alice bob -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
bob ##> ("/c " <> gLink)
@@ -4032,14 +4361,14 @@ testGroupLinkIncognitoMembership =
concurrentlyN_
[ do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /info alice to print out this incognito profile again",
bob <## "use /i alice to print out this incognito profile again",
alice <## (bobIncognito <> ": contact is connected")
]
bob #$> ("/incognito off", id, "ok")
-- alice creates group
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice <## "to add members use /a team <name> or /create link #team"
-- alice invites bob
alice ##> ("/a team " <> bobIncognito)
concurrentlyN_
@@ -4062,7 +4391,7 @@ testGroupLinkIncognitoMembership =
concurrentlyN_
[ 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 <## "use /i cath to print out this incognito profile again"
bob <## "cath invited to group #team via your group link"
bob <## "#team: cath joined the group",
do
@@ -4088,12 +4417,12 @@ testGroupLinkIncognitoMembership =
concurrentlyN_
[ 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 <## ("use /i " <> danIncognito <> " to print out this incognito profile again")
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 <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
dan <## ("#team: you joined the group incognito as " <> danIncognito)
dan
<### [ "#team: member alice (Alice) is connected",
@@ -4136,6 +4465,134 @@ testGroupLinkIncognitoMembership =
cath <# ("#team " <> danIncognito <> "> how is it going?")
]
testGroupLinkUnusedHostContactDeleted :: IO ()
testGroupLinkUnusedHostContactDeleted =
testChat2 aliceProfile bobProfile $
\alice bob -> do
-- create group 1
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
gLinkTeam <- getGroupLink alice "team" True
bob ##> ("/c " <> gLinkTeam)
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"
]
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice ##> "/create link #club"
gLinkClub <- getGroupLink alice "club" True
bob ##> ("/c " <> gLinkClub)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #club..."
concurrentlyN_
[ alice
<### [ "bob_1 (Bob): contact is connected",
"contact bob_1 is merged into bob",
"use @bob <message> to send messages",
EndsWith "invited to group #club via your group link",
EndsWith "joined the group"
],
bob
<### [ "alice_1 (Alice): contact is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#club: you joined the group"
]
]
-- list contacts
bob ##> "/cs"
bob <## "alice (Alice)"
-- delete group 1, host contact and profile are kept
bobLeaveDeleteGroup alice bob "team"
bob ##> "/cs"
bob <## "alice (Alice)"
bob `hasContactProfiles` ["alice", "bob"]
-- delete group 2, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "club"
bob ##> "/cs"
(bob </)
bob `hasContactProfiles` ["bob"]
where
bobLeaveDeleteGroup alice bob group = do
bob ##> ("/l " <> group)
concurrentlyN_
[ do
bob <## ("#" <> group <> ": you left the group")
bob <## ("use /d #" <> group <> " to delete the group"),
alice <## ("#" <> group <> ": bob left the group")
]
bob ##> ("/d #" <> group)
bob <## ("#" <> group <> ": you deleted the group")
testGroupLinkIncognitoUnusedHostContactsDeleted :: IO ()
testGroupLinkIncognitoUnusedHostContactsDeleted =
testChat2 aliceProfile bobProfile $
\alice bob -> do
bob #$> ("/incognito on", id, "ok")
bobIncognitoTeam <- createGroupBobIncognito alice bob "team" "alice"
bobIncognitoClub <- createGroupBobIncognito alice bob "club" "alice_1"
bobIncognitoTeam `shouldNotBe` bobIncognitoClub
-- list contacts
bob ##> "/cs"
bob <## "i alice (Alice)"
bob <## "i alice_1 (Alice)"
bob `hasContactProfiles` ["alice", "alice", "bob", T.pack bobIncognitoTeam, T.pack bobIncognitoClub]
-- delete group 1, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "team" bobIncognitoTeam
bob ##> "/cs"
bob <## "i alice_1 (Alice)"
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognitoClub]
-- delete group 2, unused host contact and profile are deleted
bobLeaveDeleteGroup alice bob "club" bobIncognitoClub
bob ##> "/cs"
(bob </)
bob `hasContactProfiles` ["bob"]
where
createGroupBobIncognito alice bob group bobsAliceContact = do
alice ##> ("/g " <> group)
alice <## ("group #" <> group <> " is created")
alice <## ("to add members use /a " <> group <> " <name> or /create link #" <> group)
alice ##> ("/create link #" <> group)
gLinkTeam <- getGroupLink alice group True
bob ##> ("/c " <> gLinkTeam)
bobIncognito <- getTermLine bob
bob <## "connection request sent incognito!"
alice <## (bobIncognito <> ": accepting request to join group #" <> group <> "...")
_ <- getTermLine bob
concurrentlyN_
[ do
alice <## (bobIncognito <> ": contact is connected")
alice <## (bobIncognito <> " invited to group #" <> group <> " via your group link")
alice <## ("#" <> group <> ": " <> bobIncognito <> " joined the group"),
do
bob <## (bobsAliceContact <> " (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i " <> bobsAliceContact <> " to print out this incognito profile again")
bob <## ("#" <> group <> ": you joined the group incognito as " <> bobIncognito)
]
pure bobIncognito
bobLeaveDeleteGroup alice bob group bobIncognito = do
bob ##> ("/l " <> group)
concurrentlyN_
[ do
bob <## ("#" <> group <> ": you left the group")
bob <## ("use /d #" <> group <> " to delete the group"),
alice <## ("#" <> group <> ": " <> bobIncognito <> " left the group")
]
bob ##> ("/d #" <> group)
bob <## ("#" <> group <> ": you deleted the group")
testSwitchContact :: IO ()
testSwitchContact =
testChat2 aliceProfile bobProfile $
@@ -4167,6 +4624,68 @@ testSwitchGroupMember =
bob #> "#team hi"
alice <# "#team bob> hi"
testMarkContactVerified :: IO ()
testMarkContactVerified =
testChat2 aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob
alice ##> "/i bob"
bobInfo alice
alice <## "connection not verified, use /code command to see security code"
alice ##> "/code bob"
bCode <- getTermLine alice
bob ##> "/code alice"
aCode <- getTermLine bob
bCode `shouldBe` aCode
alice ##> "/verify bob 123"
alice <##. "connection not verified, current code is "
alice ##> ("/verify bob " <> aCode)
alice <## "connection verified"
alice ##> "/i bob"
bobInfo alice
alice <## "connection verified"
alice ##> "/verify bob"
alice <##. "connection not verified, current code is "
alice ##> "/i bob"
bobInfo alice
alice <## "connection not verified, use /code command to see security code"
where
bobInfo alice = do
alice <## "contact ID: 2"
alice <## "receiving messages via: localhost"
alice <## "sending messages via: localhost"
alice <## "you've shared main profile with this contact"
testMarkGroupMemberVerified :: IO ()
testMarkGroupMemberVerified =
testChat2 aliceProfile bobProfile $ \alice bob -> do
createGroup2 "team" alice bob
alice ##> "/i #team bob"
bobInfo alice
alice <## "connection not verified, use /code command to see security code"
alice ##> "/code #team bob"
bCode <- getTermLine alice
bob ##> "/code #team alice"
aCode <- getTermLine bob
bCode `shouldBe` aCode
alice ##> "/verify #team bob 123"
alice <##. "connection not verified, current code is "
alice ##> ("/verify #team bob " <> aCode)
alice <## "connection verified"
alice ##> "/i #team bob"
bobInfo alice
alice <## "connection verified"
alice ##> "/verify #team bob"
alice <##. "connection not verified, current code is "
alice ##> "/i #team bob"
bobInfo alice
alice <## "connection not verified, use /code command to see security code"
where
bobInfo alice = do
alice <## "group ID: 1"
alice <## "member ID: 2"
alice <## "receiving messages via: localhost"
alice <## "sending messages via: localhost"
withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a
withTestChatContactConnected dbPrefix action =
withTestChat dbPrefix $ \cc -> do
@@ -4257,7 +4776,7 @@ createGroup2 gName cc1 cc2 = do
name2 <- userName cc2
cc1 ##> ("/g " <> gName)
cc1 <## ("group #" <> gName <> " is created")
cc1 <## ("use /a " <> gName <> " <name> to add members")
cc1 <## ("to add members use /a " <> gName <> " <name> or /create link #" <> gName)
addMember gName cc1 cc2 GRAdmin
cc2 ##> ("/j " <> gName)
concurrently_
@@ -4482,3 +5001,16 @@ getGroupLink cc gName created = do
cc <## ("to show it again: /show link #" <> gName)
cc <## ("to delete it: /delete link #" <> gName <> " (joined members will remain connected to you)")
pure link
hasContactProfiles :: TestCC -> [ContactName] -> Expectation
hasContactProfiles cc names =
getContactProfiles cc >>= \ps -> ps `shouldMatchList` names
getContactProfiles :: TestCC -> IO [ContactName]
getContactProfiles cc = do
user_ <- readTVarIO (currentUser $ chatController cc)
case user_ of
Nothing -> pure []
Just user -> do
profiles <- withTransaction (chatStore $ chatController cc) $ \db -> getUserContactProfiles db user
pure $ map (\Profile {displayName} -> displayName) profiles
+1 -1
View File
@@ -89,7 +89,7 @@ testProfile :: Profile
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, groupPreferences = testGroupPreferences}
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences}
decodeChatMessageTest :: Spec
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
+1
View File
@@ -26,6 +26,7 @@ module.exports = function (ty) {
ty.addPassthroughCopy("src/blog/images")
ty.addPassthroughCopy("src/images")
ty.addPassthroughCopy("src/CNAME")
ty.addPassthroughCopy("src/.well-known")
ty.addCollection('blogs', function (collection) {
return collection.getFilteredByGlob('src/blog/*.md').reverse()
+15
View File
@@ -0,0 +1,15 @@
# .well-known
This website files allow opening SimpleX Chat links (1-time invitations, contact addresses and groups) directly in the app.
## Android
File `assetlinks.json` includes certificate hashes for:
- Play Store (5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D)
- APK in GitHub releases (3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85)
- F-Droid (AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6)
## iOS
`apple-app-site-association` currently does not work, as it needs to be served with `Content-type: application/json; charset=utf-8` and GitHub pages do not support adding this header to files without JSON extension.
@@ -0,0 +1,25 @@
{
"applinks": {
"details": [
{
"appIDs": [
"5NN7GUYB6.chat.simplex.app"
],
"components": [
{
"/": "/contact/*"
},
{
"/": "/contact"
},
{
"/": "/invitation/*"
},
{
"/": "/invitation"
}
]
}
]
}
}
+16
View File
@@ -0,0 +1,16 @@
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "chat.simplex.app",
"sha256_cert_fingerprints": [
"5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D",
"3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85",
"AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6"
]
}
}
]
@@ -0,0 +1,15 @@
<p>SimpleX Chat November reviews:</p>
<p><a href="https://www.privacyguides.org/real-time-communication/#simplex-chat" target="_blank">Privacy Guides</a> recommendations.</p>
<p><a href="https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/" target="_blank">Review by Mike Kuketz</a>.</p>
<p><a href="https://www.messenger-matrix.de" target="_blank">The messenger matrix</a>.</p>
<p class="mb-[12px]"><a href="https://supernova.tilde.team/detailed_reviews.html#simplex" target="_blank">Supernova review</a> and <a href="https://supernova.tilde.team/messengers.html" target="_blank">messenger ratings</a>.</p>
<p>v4.3 is released:</p>
<ul>
<li>instant voice messages!</li>
<li>irreversible deletion of sent messages for all recipients</li>
<li>improved SMP server configuration and support for server passwords</li>
<li>privacy and security improvements: protect app screen, SimpleX links security, etc.</li>
</ul>