From dcb320ca91a5cda65d7a533adb183392aa44983d Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 25 Nov 2024 17:00:52 +1100 Subject: [PATCH 01/33] Fixed a couple of incorrect queries from a previous optimisation --- .../LibSession/Config Handling/LibSession+Contacts.swift | 4 ++-- .../MessageReceiver+MessageRequests.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 331931be3f7..c1fb69cfeb6 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -203,8 +203,8 @@ internal extension LibSession { .filter(SessionThread.Columns.variant == SessionThread.Variant.contact) .filter( /// Only want to include include standard contact conversations (not blinded conversations) - ClosedGroup.Columns.threadId > SessionId.Prefix.standard.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.standard.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.standard.rawValue && + SessionThread.Columns.id < SessionId.Prefix.standard.endOfRangeString ) .select(.id) .asRequest(of: String.self) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index c9d240fbfa6..63dbeb83889 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -60,12 +60,12 @@ extension MessageReceiver { .filter(SessionThread.Columns.variant == SessionThread.Variant.contact) .filter( ( - ClosedGroup.Columns.threadId > SessionId.Prefix.blinded15.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.blinded15.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.blinded15.rawValue && + SessionThread.Columns.id < SessionId.Prefix.blinded15.endOfRangeString ) || ( - ClosedGroup.Columns.threadId > SessionId.Prefix.blinded25.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.blinded25.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.blinded25.rawValue && + SessionThread.Columns.id < SessionId.Prefix.blinded25.endOfRangeString ) ) .asRequest(of: String.self) From 5b6d7e8321f930cbbbb8b44181634c6b2d8b8166 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 27 Nov 2024 10:06:23 +1100 Subject: [PATCH 02/33] fix an issue of keyboard not activating correctly in after following a link --- Session/Conversations/ConversationVC+Interaction.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ba1669fcb97..d900174c7c5 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1244,7 +1244,9 @@ extension ConversationVC: ) ) - self.present(modal, animated: true) + self.present(modal, animated: true) { [weak self] in + self?.hideInputAccessoryView() + } } func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies) { From ae81c4e190322f2b4d8e988a2fcc8ddd75dfef95 Mon Sep 17 00:00:00 2001 From: Bilb <1544279+Bilb@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:35:38 +0000 Subject: [PATCH 03/33] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 837 ++++++++---------- 1 file changed, 346 insertions(+), 491 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index f08574087c9..60878fd72eb 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -14982,7 +14982,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تمت إزالة {name} و{count} آخرين من منصبهم كمسؤولين." + "value" : "تمت إزالة {name} و{count} آخرين من منصبهم كمشرفين." } }, "az" : { @@ -15449,7 +15449,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تمت إزالة {name} و{other_name} من منصبي المسؤول." + "value" : "تمت إزالة {name} و{other_name} من منصبي المشرف." } }, "az" : { @@ -16395,7 +16395,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فشل في إزالة {name} و{count} آخرين كمسؤول." + "value" : "فشل في إزالة {name} و{count} آخرين كمشرف." } }, "az" : { @@ -16862,7 +16862,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فشل في إزالة {name} و{other_name} كمسؤول." + "value" : "فشل في إزالة {name} و{other_name} كمشرف." } }, "az" : { @@ -28022,7 +28022,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ألبوم بدون إسم" + "value" : "البوم بدون اسم" } }, "az" : { @@ -71063,11 +71063,35 @@ "callsPermissionsRequiredDescription1" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يمكنك تمكين إذن \"المكالمات الصوتية والفيديو\" في إعدادات الأذونات." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oprávnění pro \"Hlasové a video hovory\" můžete povolit v nastavení Oprávnění." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You can enable the \"Voice and Video Calls\" permission in Permissions Settings." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "U kunt de machtiging voor \"Spraak- en video-oproepen\" inschakelen in de privacy-instellingen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви можете увімкнути «Голосові та відеодзвінки» в налаштуваннях конфіденційності." + } } } }, @@ -74856,7 +74880,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Дозволяє здійснювати голосові та відео виклики з іншими користувачами." + "value" : "Дозволяє голосові та відеодзвінки з іншими користувачами." } }, "ur-IN" : { @@ -75814,7 +75838,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ви пропустили дзвінок від {name}, бо не увімкнули Голосові та Відеодзвінки у Налаштуваннях Приватності." + "value" : "Ви пропустили дзвінок від {name}, бо не увімкнули Голосові та відеодзвінки у налаштуваннях конфіденційності." } }, "ur-IN" : { @@ -115436,7 +115460,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Обрізка повідомлень" + "value" : "Автовидалення повідомлень" } }, "ur-IN" : { @@ -115915,7 +115939,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Обрізати спільноти" + "value" : "Автовидалення повідомлень спільнот" } }, "ur-IN" : { @@ -122704,49 +122728,49 @@ "af" : { "stringUnit" : { "state" : "translated", - "value" : "Ons het opgemerk {app_name} neem lank om te begin.

Jy kan aanhou wag, jou toestel logs uitvoer om te deel vir foutsporing, of probeer om Session te herbegin." + "value" : "Ons het opgemerk {app_name} neem lank om te begin.

Jy kan aanhou wag, jou toestel logs uitvoer om te deel vir foutsporing, of probeer om {app_name} te herbegin." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لقد لاحظنا أن {app_name} يستغرق وقتًا طويلاً لبدء.

يمكنك مواصلة الانتظار، تصدير سجلات الجهاز للمشاركة في استكشاف الأخطاء وإصلاحها، أو محاولة إعادة تشغيل Session." + "value" : "لقد لاحظنا أن {app_name} يستغرق وقتًا طويلاً لبدء.

يمكنك مواصلة الانتظار، تصدير سجلات الجهاز للمشاركة في استكشاف الأخطاء وإصلاحها، أو محاولة إعادة تشغيل {app_name}." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} tətbiqinin başladılmasının çox vaxt apardığına fikir verdik.

Gözləməyə davam edə, problemin aradan qaldırılması üçün cihazınızın jurnallarını xaricə köçürə və ya Session-u yenidən başlatmağa çalışa bilərsiniz." + "value" : "{app_name} tətbiqinin başladılmasının çox vaxt apardığına fikir verdik.

Gözləməyə davam edə, problemin aradan qaldırılması üçün cihazınızın jurnallarını xaricə köçürə və ya {app_name}-u yenidən başlatmağa çalışa bilərsiniz." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ما پٹِن گی اتہ {app_name} سرا کتن وقت گشتنت۔

باقی انتظار بکنت، سپاڈی بروک کردیا گزارش بکنت بیت ائی٬ گپچہ رفتارشت بار بکودنت بیت اتہ بھال دکنت۔" + "value" : "ما دیستگ کہ {app_name} ءِ بندات کنگ ءَ بازیں وھدے لگ اِیت۔

شما دیم ءَ اوشتات کن اِت، وتی ڈیوائس ءِ لاگاں پہ جیڑہ ءِ گیش ءُ گیوار ءَ شیئر کنگ ءِ ھاترا برآمد کن اِت یا {app_name} ءَ پدا بندات کنگ ءِ جُھد ءَ کن اِت۔" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Мы заўважылі, што {app_name} патрабуе шмат часу для запуску.

Вы можаце працягваць чакаць, экспартаваць журналы вашай прылады для спагнання праблем, альбо паспрабаваць перазапусціць Session." + "value" : "Мы заўважылі, што {app_name} патрабуе шмат часу для запуску.

Вы можаце працягваць чакаць, экспартаваць журналы вашай прылады для спагнання праблем, альбо паспрабаваць перазапусціць {app_name}." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Забелязахме, че стартирането на {app_name} отнема много време.

Можете да продължите да чакате, да експортирате дневници на устройството си, за да ги споделите за отстраняване на неизправности, или да опитате да рестартирате Session." + "value" : "Забелязахме, че стартирането на {app_name} отнема много време.

Можете да продължите да чакате, да експортирате дневници на устройството си, за да ги споделите за отстраняване на неизправности, или да опитате да рестартирате {app_name}." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session." + "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Hem notat que {app_name} està trigant molt a començar.

Podeu continuar esperant, exportar els registres del dispositiu per compartir-los per solucionar problemes, o intentar reiniciar Session." + "value" : "Hem notat que {app_name} està trigant molt a començar.

Podeu continuar esperant, exportar els registres del dispositiu per compartir-los per solucionar problemes, o intentar reiniciar {app_name}." } }, "cs" : { @@ -122764,7 +122788,7 @@ "da" : { "stringUnit" : { "state" : "translated", - "value" : "Vi har bemærket, at {app_name} tager lang tid at starte.

Du kan fortsætte med at vente, eksportere dine enhedslogfiler for at dele dem til fejlfinding eller prøve at genstarte Session." + "value" : "Vi har bemærket, at {app_name} tager lang tid at starte.

Du kan fortsætte med at vente, eksportere dine enhedslogfiler for at dele dem til fejlfinding eller prøve at genstarte {app_name}." } }, "de" : { @@ -122776,7 +122800,7 @@ "el" : { "stringUnit" : { "state" : "translated", - "value" : "Παρατηρήσαμε ότι το {app_name} χρειάζεται πολύ χρόνο για να ξεκινήσει.

Μπορείτε να συνεχίσετε να περιμένετε, να εξάγετε τα αρχεία καταγραφής της συσκευής σας για να τα μοιραστείτε για την αντιμετώπιση προβλημάτων ή να επανεκκινήσετε το Session." + "value" : "Παρατηρήσαμε ότι το {app_name} χρειάζεται πολύ χρόνο για να ξεκινήσει.

Μπορείτε να συνεχίσετε να περιμένετε, να εξάγετε τα αρχεία καταγραφής της συσκευής σας για να τα μοιραστείτε για την αντιμετώπιση προβλημάτων ή να επανεκκινήσετε το {app_name}." } }, "en" : { @@ -122806,13 +122830,13 @@ "et" : { "stringUnit" : { "state" : "translated", - "value" : "Oleme märganud, et {app_name} käivitamine võtab kaua aega.

Võite jätkata ootamist, eksportida oma seadme logisid tõrkeotsingu eesmärgil jagamiseks või proovida Session'i taaskäivitamist." + "value" : "Oleme märganud, et {app_name} käivitamine võtab kaua aega.

Võite jätkata ootamist, eksportida oma seadme logisid tõrkeotsingu eesmärgil jagamiseks või proovida {app_name}'i taaskäivitamist." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} martxan jartzeko denbora gehiegi hartzen ari dela nabaritu dugu.

Jarrai itzazu itxaroten, esportatu zure gailu-erregistroak konpontzeko partekatzeko edo saiatu Session berrabiarazten." + "value" : "{app_name} martxan jartzeko denbora gehiegi hartzen ari dela nabaritu dugu.

Jarrai itzazu itxaroten, esportatu zure gailu-erregistroak konpontzeko partekatzeko edo saiatu {app_name} berrabiarazten." } }, "fa" : { @@ -122824,31 +122848,31 @@ "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Huomasimme, että {app_name} käynnistyy hitaasti.

Voit jatkaa odottamista, viedä laitteesi lokit jaettavaksi vianmäärityksessä tai yrittää käynnistää Sessionin uudelleen." + "value" : "Huomasimme, että {app_name} käynnistyy hitaasti.

Voit jatkaa odottamista, viedä laitteesi lokit jaettavaksi vianmäärityksessä tai yrittää käynnistää {app_name} uudelleen." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Napansin naming matagal bago mag-start ang {app_name}.

Puwede kang maghintay nalang, i-export ang iyong device logs para i-share para sa troubleshooting, o subukan i-restart ang Session." + "value" : "Napansin naming matagal bago mag-start ang {app_name}.

Puwede kang maghintay nalang, i-export ang iyong device logs para i-share para sa troubleshooting, o subukan i-restart ang {app_name}." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nous avons remarqué que {app_name} met beaucoup de temps à démarrer.

Vous pouvez continuer à attendre, exporter les journaux de votre appareil pour les partager pour le dépannage ou essayer de redémarrer Session." + "value" : "Nous avons remarqué que {app_name} met beaucoup de temps à démarrer.

Vous pouvez continuer à attendre, exporter les journaux de votre appareil pour les partager pour le dépannage ou essayer de redémarrer {app_name}." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Notamos que {app_name} está a tardar moito en iniciar.

Podes esperar, exportar os teus rexistros do dispositivo para compartir e solucionar problemas, ou tentar reiniciar Session." + "value" : "Notamos que {app_name} está a tardar moito en iniciar.

Podes esperar, exportar os teus rexistros do dispositivo para compartir e solucionar problemas, ou tentar reiniciar {app_name}." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Mun lura cewa {app_name} yana ɗaukar dogon lokaci don farawa.

Za ku iya ci gaba da jira, fitar da log ɗin na'urarku don rabawa don magance matsaloli, ko sake farawa Session." + "value" : "Mun lura cewa {app_name} yana ɗaukar dogon lokaci don farawa.

Za ku iya ci gaba da jira, fitar da log ɗin na'urarku don rabawa don magance matsaloli, ko sake farawa {app_name}." } }, "he" : { @@ -122866,7 +122890,7 @@ "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Primijetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti čekati, izvesti zapise uređaja za dijeljenje radi rješavanja problema ili pokušati ponovo pokrenuti Session." + "value" : "Primijetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti čekati, izvesti zapise uređaja za dijeljenje radi rješavanja problema ili pokušati ponovo pokrenuti {app_name}." } }, "hu" : { @@ -122902,7 +122926,7 @@ "ka" : { "stringUnit" : { "state" : "translated", - "value" : "გავიგეთ {app_name}-ის გაშვება დიდ დროს იკავებს.

თქვენ შეგიძლიათ დაელოდოთ, ექსპორტირდეთ თქვენი მოწყობილობის ჟურნალები რათა გაუზიაროთ პრობლემების დიაგნოსტირებისთვის, ან სცადოთ Session-ის გადატვირთვა." + "value" : "გავიგეთ {app_name}-ის გაშვება დიდ დროს იკავებს.

თქვენ შეგიძლიათ დაელოდოთ, ექსპორტირდეთ თქვენი მოწყობილობის ჟურნალები რათა გაუზიაროთ პრობლემების დიაგნოსტირებისთვის, ან სცადოთ {app_name}-ის გადატვირთვა." } }, "km" : { @@ -122974,7 +122998,7 @@ "my" : { "stringUnit" : { "state" : "translated", - "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session." + "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." } }, "nb" : { @@ -123010,7 +123034,7 @@ "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Timazindikira {app_name} kutenga nthawi kuti ayambe.

Inu mungapitirize kudikira, kutulutsira chipangizo malipoti kuti azipeza mavuto, kapena yesani kuyambiranso Session." + "value" : "Timazindikira {app_name} kutenga nthawi kuti ayambe.

Inu mungapitirize kudikira, kutulutsira chipangizo malipoti kuti azipeza mavuto, kapena yesani kuyambiranso {app_name}." } }, "pa-IN" : { @@ -135222,6 +135246,46 @@ } } } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити це повідомлення?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення?" + } + } + } + } + } + } } } }, @@ -138451,6 +138515,58 @@ "deleteMessageDescriptionDevice" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسالة من هذا الجهاز فقط؟" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل من هذا الجهاز فقط؟" + } + } + } + } + } + } + }, "cs" : { "stringUnit" : { "state" : "translated", @@ -138491,6 +138607,34 @@ } } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, du möchtest diese Nachrichten nur von diesem Gerät löschen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du diese Nachrichten wirklich nur von diesem Gerät löschen?" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -138586,6 +138730,46 @@ } } } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення лише з цього пристрою?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення лише з цього пристрою?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити це повідомлення лише з цього пристрою?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви дійсно хочете видалити ці повідомлення лише з цього пристрою?" + } + } + } + } + } + } } } }, @@ -142835,6 +143019,58 @@ "deleteMessageNoteToSelfWarning" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف هذه الرسالة من جميع أجهزتك" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن حذف بعض الرسائل التي حددتها من جميع أجهزتك" + } + } + } + } + } + } + }, "cs" : { "stringUnit" : { "state" : "translated", @@ -157430,7 +157666,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "{name} قام/ت بإيقاف تشغيل الرسائل المختفية إيقاف." + "value" : "{name} قام بإيقاف تشغيل الرسائل المختفية إيقاف." } }, "az" : { @@ -187785,11 +188021,29 @@ "groupDeletedMemberDescription" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupina {group_name} byla smazána správcem skupiny. Nebudete moci posílat další zprávy." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{group_name} has been deleted by a group admin. You will not be able to send any more messages." } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} is verwijderd door een groepsbeheerder. U kunt geen berichten meer versturen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} видалено адміністратором групи. Ви більше не зможете надсилати повідомлення." + } } } }, @@ -193789,6 +194043,46 @@ } } } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошень" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошень" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошення" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилання запрошень" + } + } + } + } + } + } } } }, @@ -203811,460 +204105,10 @@ "groupMemberNewYouHistoryTwo" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jy en {name} is genooi om by die groep aan te sluit. Kletsgeskiedenis is gedeel." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "أنت و{name} انضموا للمجموعة. تمت مشاركة سجل الدردشة." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siz{name} qrupa qoşulmaq üçün dəvət edildiniz. Söhbət tarixçəsi paylaşıldı." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "Šumār a {name} šumār zant group ke. Chat history was shared." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы і {name} былі запрошаны далучыцца да групы. Гісторыя чатаў была абагулена." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вие и {name} бяхте поканени да се присъедините към групата. История на чатовете беше споделена." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনি এবং {name} গ্রুপে যোগ দেওয়ার জন্য আমন্ত্রিত হয়েছে। চ্যাট ইতিহাস শেয়ার করা হয়েছে।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu i {name} heu estat convidats a unir-vos al grup. S'ha compartit l'historial de la conversa." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vy a {name} byli pozváni do skupiny. Historie konverzace byla sdílena." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chi a {name} ymunodd â'r grŵp. Hanes sgwrs wedi cael ei rhannu." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} blev inviteret til at deltage i gruppen. Chat historik blev delt." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du und {name} wurden eingeladen, der Gruppe beizutreten. Der Chatverlauf wurde freigegeben." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Εσείς και {name} προσκληθήκατε να συμμετάσχετε στην ομάδα. Το ιστορικό συνομιλίας κοινοποιήθηκε." - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "You and {name} were invited to join the group. Chat history was shared." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi kaj {name} estis invititaj aniĝi al la grupo. Babilhistorio estis dividita." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : " y {name} fueron invitados a unirse al grupo. El historial de chat fue compartido." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : " y {name} fueron invitados a unirse al grupo. El historial de chat fue compartido." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sind ja {name} kutsuti grupiga liituma. Vestluse ajalugu jagati nendega." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zuk eta {name} taldera batzeko gonbidatu zaituzte. Txat historia partekatu da." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "شما و {name} دعوت شدید تا به گروه بپیوندید. تاریخچه ی چت به اشتراک گذاشته شد." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sinä ja {name} kutsuttiin ryhmään. Keskusteluhistoria jaetaan." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikaw at {name} ay naimbitahan na sumali sa grupo. Ibinahagi ang kasaysayan ng chat." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vous et {name} avez été invité·e·s à rejoindre le groupe. L'historique de discussion a été partagé." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ku da {name} an gayyace ku shiga ƙungiyar. An raba tarihin hira." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "את/ה ו{name}‏ הוזמנתם להצטרף לקבוצה. היסטוריית הצ'אט שותפה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आप और {name} को समूह में शामिल होने के लिए आमंत्रित किया गया। चैट इतिहास साझा किया गया।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi i {name} pozvani ste da se pridružite grupi. Povijest razgovora je podijeljena." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Te és {name} meg lettetek hívva a csoportba. A beszélgetési előzményeket megosztottuk." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Դուք և {name}֊ը հրավիրվել են միանալու խմբին: Զրույցի պատմությունը կիսվել է:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anda dan {name} telah diundang untuk bergabung dengan grup. Riwayat obrolan dibagikan." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu e {name} avete ricevuto un invito a unirvi al gruppo. La cronologia della chat è condivisa." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "あなた{name} がグループに招待されました。チャット履歴が共有されました。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენ და {name} მიწვეული იყავით ჯგუფში. ჩეთის ისტორია გაზიარდა." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "អ្នក និង {name} ត្រូវបានអញ្ជើញឱ្យចូលក្រុមនេះ។បានចែករំលែកប្រវត្តិការជជែក។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನೀವು ಮತ್ತು {name} ಅವರಿಗೆ ಗುಂಪಿಗೆ ಸೇರಲು ಆಹ್ವಾನಿಸಲಾಗಿದೆ. ಚಾಟ್ ಇತಿಹಾಸವನ್ನು ಹಂಚಲಾಗಿದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "당신{name}이 그룹에 초대받았습니다. 대화 내역이 공개됩니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "تۆ و {name} بانگکران بۆ بەشداریکردن لە گروپەکە. مێژوو بگردەوەیی پەیامەکان سییبرەیە." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Te û {name} hatin dawetin ku tevlî komê bibin. Dîroka sohbetê hate parve kirin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ggwe ne {name} mwakuyitibwa okwegatta mu kibiina. Ebika by'obubaka by'akugabana." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūs ir {name} buvo pakviesti prisijungti prie grupės. Pokalbio istorija buvo pasidalinta." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вие и {name} беа поканети да се придружат на групата. Историјата на разговорот е споделена." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Та болон {name} бүлэгт нэгдэх урилга авсан байна. Чатын түүх хуваалцагдсан." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anda dan {name} dijemput untuk menyertai kumpulan. Sejarah sembang telah dikongsi." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်နှင့် {name} အဖွဲ့သို့ ဖိတ်ကြားခံရပြီ။ စကားဝိုင်းမှတ်တမ်းကိုမျှဝေခဲ့သည်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} ble invitert til gruppen. Chat-historikk ble delt." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} ble invitert til å bli med i gruppen. Chat-historikk ble delt." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईं{name}लाई समूहमा सामेल हुन आमन्त्रित गरियो। च्याट इतिहास सेयर गरियो।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "U en {name} zijn uitgenodigd om lid te worden van de groep. Geschiedenis van het gesprek is gedeeld." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du og {name} vart invitert til å bli med i gruppa. Chathistorikk vart delt." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inu ndi {name} anaitanidwa kuti alowe mu gulu. Mbiri ya macheza idagawidwa." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ty oraz użytkownik {name} zostaliście zaproszeni do grupy. Udostępniono historię czatu." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "تاسو او {name} ډله کې ګډون کولو ته بلل شوی. د خبرو تاریخ شریک شوی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Você e {name} foram convidados a participar do grupo. O histórico de conversas foi compartilhado." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Você e {name} foram convidados a juntar-se ao grupo. O histórico da conversa foi partilhado." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu și {name} ați fost invitați să vă alăturați grupului. Istoricul conversațiilor a fost partajat." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы и пользователь {name} приглашены вступить в группу. История чата была передана." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ti i {name} ste pozvani da se pridružite grupi. Istorija razgovora je deljena." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබ සහ {name} කණ්ඩායමට සම්බන්ධ වන්නට ආරාධනා කරන ලදී. සංවාද ඉතිහාසය බෙදා ගන්නා ලදී." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vy a {name} ste boli pozvaní, aby ste sa pripojili do skupiny. História chatu bola zdieľaná." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi in {name} sta bila povabljena, da se pridružita skupini. Zgodovina klepeta je bila deljena." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ju dhe {name} u ftuat të bashkoheni me grupin. Historia e bisedës u ndanë." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви и {name} су позвани да се придруже групи. Историја ћаскања је подељена." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi i {name} ste pozvani da se pridružite grupi. Istorija četa je podeljena." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du och {name} bjöds in att gå med i gruppen. Chatt historik delades." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wewe na {name} mmealikwa kujiunga na kundi. Historia ya gumzo ilishirikiwa." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "நீங்கள் மற்றும் {name} குழுவில் சேர்க்கப்பட்டீர்கள். உரையாடல் வரலாறு பகிரப்பட்டது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీరు మరియు {name} సమూహంలో చేరడానికి ఆహ్వానించబడ్డారు. చాట్ చరిత్ర పంచబడింది." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "คุณ และ {name} ถูกเชิญเข้าร่วมกลุ่ม ประวัติการแชทถูกแชร์" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sen ve {name} gruba katılmaya davet edildiniz. Sohbet geçmişi paylaşıldı." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви та {name} були запрошені приєднатися до групи. Було надано спільний доступ до історії чату." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آپ اور {name} گروپ میں شامل ہونے کی دعوت دی گئی۔ چیٹ تاریخ شیئر کی گئی۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siz va {name} guruhga qo'shildi. Suhbat tarixini ko'rish imkoniyati berilgan." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bạn{name} đã được mời tham gia nhóm. Lịch sử trò chuyện đã được chia sẻ." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mna kunye {name} babememelwe ukuba bajoyine iqela. Imbali yencoko yenziwe yabelwana ngayo." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name}被邀请加入了群组。 聊天记录已共享。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name} 加入了群組。聊天記錄已分享。" + "value" : "You and {other_name} were invited to join the group. Chat history was shared." } } } @@ -209567,6 +209411,17 @@ } } }, + "groupPendingRemoval" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pending removal" + } + } + } + }, "groupPromotedYou" : { "extractionState" : "manual", "localizations" : { @@ -226652,7 +226507,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Toggle system menu bar visibility" + "value" : "تبديل رؤية شريط قائمة النظام" } }, "az" : { @@ -235544,7 +235399,7 @@ "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Previzualizări ale linkurilor" + "value" : "Podgląd linków" } }, "ps" : { @@ -238538,7 +238393,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Надсилати попередній перегляд посилань" + "value" : "Попередній перегляд надісланих посилань" } }, "ur-IN" : { @@ -289119,7 +288974,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التنبيهات - الكل" + "value" : "الإشعارات - الكل" } }, "az" : { @@ -289586,7 +289441,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التنبيهات - الإشعارات فقط" + "value" : "الإشعارات- الإشارات فقط" } }, "az" : { @@ -290053,7 +289908,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التنبيهات - مكتومة" + "value" : "الإشعارات - مكتومة" } }, "az" : { @@ -298149,7 +298004,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Стратегія сповіщення" + "value" : "Принцип оповіщення" } }, "ur-IN" : { @@ -381688,7 +381543,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "System Information: {information}" + "value" : "معلومات النظام: {information}" } }, "az" : { @@ -384994,7 +384849,7 @@ "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Бачити та надсилати індикатори набору тексту." + "value" : "Бачити та надсилати індикатори введення тексту." } }, "ur-IN" : { From 2ee1fa01253e4f25da381e9833c16b1b11835434 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 27 Nov 2024 17:17:56 +1100 Subject: [PATCH 04/33] Updated NTS & 1-1 conversation deletion to be consistent with other plats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added a few functions to retrieve conversation settings from libSession • Updated the Note to Self swipe action to be "Hide" (hides the conversation but does not delete the messages) • Updated the one-to-one deletion behaviour (now syncs both hiding the conversation and deleting it's messages) • Updated the logic to retrieve the relevant disappearing messages setting from libSession when creating a thread if it doesn't exist (allows us to delete threads without worrying about losing settings) • Updated a bunch of dependency management & injection code so the unit tests would pass --- .gitmodules | 2 +- Session/Closed Groups/NewClosedGroupVC.swift | 23 +- .../ConversationVC+Interaction.swift | 30 ++- .../Settings/ThreadSettingsViewModel.swift | 10 +- .../GlobalSearchViewController.swift | 8 +- Session/Home/HomeVC.swift | 6 +- .../MessageRequestsViewModel.swift | 2 +- .../New Conversation/NewMessageScreen.swift | 9 +- .../StartConversationScreen.swift | 15 +- Session/Meta/SessionApp.swift | 12 +- Session/Notifications/AppNotifications.swift | 3 +- .../PushRegistrationManager.swift | 10 +- .../PushRegistrationManagerType.swift | 0 Session/Onboarding/Onboarding.swift | 10 +- Session/Open Groups/JoinOpenGroupVC.swift | 18 +- Session/Settings/QRCodeScreen.swift | 10 +- Session/Settings/SettingsViewModel.swift | 6 +- Session/Utilities/MockDataGenerator.swift | 55 ++-- .../UIContextualAction+Utilities.swift | 9 +- .../Calls/NoopSessionCallManager.swift | 0 .../Migrations/_013_SessionUtilChanges.swift | 10 +- .../Database/Models/SessionThread.swift | 243 +++++++++++++++--- .../Config Handling/LibSession+Contacts.swift | 79 +++--- .../Config Handling/LibSession+Shared.swift | 155 +++++++++++ .../LibSession+UserProfile.swift | 55 ++-- .../QueryInterfaceRequest+Utilities.swift | 2 +- .../LibSession+SessionMessagingKit.swift | 3 +- .../Open Groups/OpenGroupManager.swift | 17 +- .../MessageReceiver+Calls.swift | 20 +- .../MessageReceiver+ClosedGroups.swift | 17 +- .../MessageReceiver+MessageRequests.swift | 10 +- .../MessageReceiver+VisibleMessages.swift | 10 +- .../MessageSender+ClosedGroups.swift | 29 ++- .../Notifications/PushNotificationAPI.swift | 3 +- .../Jobs/Types/MessageSendJobSpec.swift | 6 +- .../NotificationServiceExtension.swift | 15 +- .../LibSession/LibSessionError.swift | 2 + 37 files changed, 709 insertions(+), 205 deletions(-) create mode 100644 Session/Notifications/PushRegistrationManagerType.swift create mode 100644 SessionMessagingKit/Calls/NoopSessionCallManager.swift diff --git a/.gitmodules b/.gitmodules index b8c1e380954..94e414a92da 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "LibSession-Util"] path = LibSession-Util - url = https://github.com/oxen-io/libsession-util.git + url = https://github.com/session-foundation/libsession-util.git diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 7620dc6a547..0280648070f 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -28,13 +28,27 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate case contacts } - private let contactProfiles: [Profile] = Profile.fetchAllContactProfiles(excludeCurrentUser: true) + private let dependencies: Dependencies + private let contactProfiles: [Profile] private lazy var data: [ArraySection] = [ ArraySection(model: .contacts, elements: contactProfiles) ] private var selectedContacts: Set = [] private var searchText: String = "" - + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.contactProfiles = Profile.fetchAllContactProfiles(excludeCurrentUser: true) + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Components private static let textFieldHeight: CGFloat = 50 @@ -329,7 +343,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate } let selectedContacts = self.selectedContacts let message: String? = (selectedContacts.count > 20 ? "deleteAfterLegacyGroupsGroupCreation".localized() : nil) - ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self, dependencies] _ in MessageSender .createClosedGroup(name: name, members: selectedContacts) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) @@ -358,7 +372,8 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate for: thread.id, variant: thread.variant, dismissing: self?.presentingViewController, - animated: false + animated: false, + using: dependencies ) } ) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ba1669fcb97..0c0630d3d56 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1256,9 +1256,15 @@ extension ConversationVC: // FIXME: Add in support for starting a thread with a 'blinded25' id guard (try? SessionId.Prefix(from: sessionId)) != .blinded25 else { return } guard (try? SessionId.Prefix(from: sessionId)) == .blinded15 else { - Storage.shared.write { db in - try SessionThread - .fetchOrCreate(db, id: sessionId, variant: .contact, shouldBeVisible: nil) + Storage.shared.write { [dependencies = viewModel.dependencies] db in + try SessionThread.upsert( + db, + id: sessionId, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) } let conversationVC: ConversationVC = ConversationVC( @@ -1277,7 +1283,7 @@ extension ConversationVC: return } - let targetThreadId: String? = Storage.shared.write { db in + let targetThreadId: String? = Storage.shared.write { [dependencies = viewModel.dependencies] db in let lookup: BlindedIdLookup = try BlindedIdLookup .fetchOrCreate( db, @@ -1287,14 +1293,14 @@ extension ConversationVC: isCheckingForOutbox: false ) - return try SessionThread - .fetchOrCreate( - db, - id: (lookup.sessionId ?? lookup.blindedId), - variant: .contact, - shouldBeVisible: nil - ) - .id + return try SessionThread.upsert( + db, + id: (lookup.sessionId ?? lookup.blindedId), + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ).id } guard let threadId: String = targetThreadId else { return } diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 61ffd19f8a9..4e04bbcc5d1 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -774,8 +774,14 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi dependencies.storage.writeAsync { [dependencies] db in let currentUserSessionId: String = getUserHexEncodedPublicKey(db, using: dependencies) try selectedUsers.forEach { userId in - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: userId, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: userId, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) try LinkPreview( url: communityUrl, diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index f6581a8c830..cac8d947dea 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -393,12 +393,14 @@ extension GlobalSearchViewController { // If it's a one-to-one thread then make sure the thread exists before pushing to it (in case the // contact has been hidden) if threadVariant == .contact { - Storage.shared.write { db in - try SessionThread.fetchOrCreate( + Storage.shared.write { [dependencies] db in + try SessionThread.upsert( db, id: threadId, variant: threadVariant, - shouldBeVisible: nil // Don't change current state + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies ) } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index f9e5e732e5c..7335d3b7da4 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -802,7 +802,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS }() let destructiveAction: UIContextualAction.SwipeAction = { switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember) { - case (.contact, true, _): return .clear + case (.contact, true, _): return .hide case (.legacyGroup, _, true), (.group, _, true), (.community, _, _): return .leave default: return .delete } @@ -897,7 +897,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS @objc func createNewConversation() { let viewController = SessionHostingViewController( - rootView: StartConversationScreen(), + rootView: StartConversationScreen(using: viewModel.dependencies), customizedNavigationBackground: .backgroundSecondary ) viewController.setNavBarTitle("conversationsStart".localized()) @@ -912,7 +912,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS } func createNewDMFromDeepLink(sessionId: String) { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen(accountId: sessionId)) + let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen(accountId: sessionId, using: viewModel.dependencies)) viewController.setNavBarTitle( "messageNew" .putNumber(1) diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index 407bf0ebf46..e4a950bbe19 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -204,7 +204,7 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O // Remove the one-to-one requests try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .deleteContactConversationAndMarkHidden, threadIds: threadInfo .filter { _, variant in variant == .contact } .map { id, _ in id }, diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index d4d5db3639d..124a8ae01ea 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -9,12 +9,14 @@ import SessionSnodeKit struct NewMessageScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies @State var tabIndex = 0 @State private var accountIdOrONS: String @State private var errorString: String? = nil - init(accountId: String = "") { + init(accountId: String = "", using dependencies: Dependencies) { + self.dependencies = dependencies self.accountIdOrONS = accountId } @@ -122,7 +124,8 @@ struct NewMessageScreen: View { variant: .contact, action: .compose, dismissing: self.host.controller, - animated: false + animated: false, + using: dependencies ) } } @@ -202,5 +205,5 @@ struct EnterAccountIdScreen: View { } #Preview { - NewMessageScreen() + NewMessageScreen(using: Dependencies()) } diff --git a/Session/Home/New Conversation/StartConversationScreen.swift b/Session/Home/New Conversation/StartConversationScreen.swift index f3ea227cb90..866a4a1436c 100644 --- a/Session/Home/New Conversation/StartConversationScreen.swift +++ b/Session/Home/New Conversation/StartConversationScreen.swift @@ -7,6 +7,11 @@ import SessionUtilitiesKit struct StartConversationScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } var body: some View { ZStack(alignment: .topLeading) { @@ -26,7 +31,9 @@ struct StartConversationScreen: View { image: "Message", title: title ) { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen()) + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: NewMessageScreen(using: dependencies) + ) viewController.setNavBarTitle(title) viewController.setUpDismissingButton(on: .right) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) @@ -46,7 +53,7 @@ struct StartConversationScreen: View { image: "Group", title: "groupCreate".localized() ) { - let viewController = NewClosedGroupVC() + let viewController = NewClosedGroupVC(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } .accessibility( @@ -64,7 +71,7 @@ struct StartConversationScreen: View { image: "Globe", // stringlint:ignore title: "communityJoin".localized() ) { - let viewController = JoinOpenGroupVC() + let viewController = JoinOpenGroupVC(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } .accessibility( @@ -155,5 +162,5 @@ fileprivate struct NewConversationCell: View { } #Preview { - StartConversationScreen() + StartConversationScreen(using: Dependencies()) } diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index fc02eb667c4..0e9b6b38f98 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -37,7 +37,8 @@ public struct SessionApp { variant: SessionThread.Variant, action: ConversationViewModel.Action = .none, dismissing presentingViewController: UIViewController?, - animated: Bool + animated: Bool, + using dependencies: Dependencies ) { let threadInfo: (threadExists: Bool, isMessageRequest: Bool)? = Storage.shared.read { db in let isMessageRequest: Bool = { @@ -84,7 +85,14 @@ public struct SessionApp { guard threadInfo?.threadExists == true else { DispatchQueue.global(qos: .userInitiated).async { Storage.shared.write { db in - try SessionThread.fetchOrCreate(db, id: threadId, variant: variant, shouldBeVisible: nil) + try SessionThread.upsert( + db, + id: threadId, + variant: variant, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) } // Send back to main thread for UI transitions diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 7d20c552836..d51f7c16445 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -576,7 +576,8 @@ class NotificationActionHandler { for: threadId, variant: threadVariant, dismissing: nil, - animated: (UIApplication.shared.applicationState == .active) + animated: (UIApplication.shared.applicationState == .active), + using: dependencies ) return Just(()) diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 3987ed35c50..04f91586572 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -315,8 +315,14 @@ public enum PushRegistrationError: Error { }() let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: caller, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: caller, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) let interaction: Interaction = try Interaction( messageUuid: uuid, diff --git a/Session/Notifications/PushRegistrationManagerType.swift b/Session/Notifications/PushRegistrationManagerType.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index f5a770bb3e2..dcf17541322 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -169,8 +169,14 @@ enum Onboarding { /// /// **Note:** We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false` /// otherwise it won't actually get synced correctly - try SessionThread - .fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false) + try SessionThread.upsert( + db, + id: x25519PublicKey, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + calledFromConfig: false, + using: dependencies + ) try SessionThread .filter(id: x25519PublicKey) diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 09b71135d12..808e294a868 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -10,6 +10,7 @@ import SessionUtilitiesKit import SignalUtilitiesKit final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate, NavigatableStateHolder { + private let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() private var disposables: Set = Set() @@ -55,6 +56,18 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC return result }() + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: - Lifecycle @@ -185,7 +198,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC isJoining = true - ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in + ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self, dependencies] _ in Storage.shared .writePublisher { db in OpenGroupManager.shared.add( @@ -240,7 +253,8 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC for: OpenGroup.idFor(roomToken: roomToken, server: server), variant: .community, dismissing: nil, - animated: false + animated: false, + using: dependencies ) } } diff --git a/Session/Settings/QRCodeScreen.swift b/Session/Settings/QRCodeScreen.swift index c7d59fe4efc..53093366063 100644 --- a/Session/Settings/QRCodeScreen.swift +++ b/Session/Settings/QRCodeScreen.swift @@ -8,11 +8,16 @@ import AVFoundation struct QRCodeScreen: View { @EnvironmentObject var host: HostWrapper + let dependencies: Dependencies @State var tabIndex = 0 @State private var accountId: String = "" @State private var errorString: String? = nil + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + var body: some View { ZStack(alignment: .topLeading) { VStack( @@ -51,7 +56,8 @@ struct QRCodeScreen: View { variant: .contact, action: .compose, dismissing: self.host.controller, - animated: false + animated: false, + using: dependencies ) } } @@ -94,5 +100,5 @@ struct MyQRCodeScreen: View { } #Preview { - QRCodeScreen() + QRCodeScreen(using: Dependencies()) } diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 003900c7ed0..0a838e60e22 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -145,7 +145,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .eraseToAnyPublisher() lazy var rightNavItems: AnyPublisher<[SessionNavItem], Never> = navState - .map { [weak self] navState -> [SessionNavItem] in + .map { [weak self, dependencies] navState -> [SessionNavItem] in switch navState { case .standard: return [ @@ -156,7 +156,9 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl style: .plain, accessibilityIdentifier: "View QR code", action: { [weak self] in - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: QRCodeScreen()) + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: QRCodeScreen(using: dependencies) + ) viewController.setNavBarTitle("qrCode".localized()) self?.transitionToScreen(viewController) } diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index c490df3d67b..7f802e4f74c 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -49,8 +49,14 @@ enum MockDataGenerator { logProgress("", "Start") // First create the thread used to indicate that the mock data has been generated - _ = try? SessionThread - .fetchOrCreate(db, id: "MockDatabaseThread", variant: .contact, shouldBeVisible: false) + _ = try? SessionThread.upsert( + db, + id: "MockDatabaseThread", + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + calledFromConfig: false, + using: dependencies + ) // MARK: - -- DM Thread @@ -74,13 +80,14 @@ enum MockDataGenerator { .randomElement(using: &dmThreadRandomGenerator) ?? 0) // Generate the thread - let thread: SessionThread = try! SessionThread - .fetchOrCreate( - db, - id: randomSessionId, - variant: .contact, - shouldBeVisible: true - ) + let thread: SessionThread = try! SessionThread.upsert( + db, + id: randomSessionId, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: false, + using: dependencies + ) // Generate the contact let contact: Contact = try! Contact( @@ -186,13 +193,14 @@ enum MockDataGenerator { members.append(randomSessionId) } - let thread: SessionThread = try! SessionThread - .fetchOrCreate( - db, - id: randomGroupPublicKey, - variant: .legacyGroup, - shouldBeVisible: true - ) + let thread: SessionThread = try! SessionThread.upsert( + db, + id: randomGroupPublicKey, + variant: .legacyGroup, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: false, + using: dependencies + ) _ = try! ClosedGroup( threadId: randomGroupPublicKey, name: groupName, @@ -316,13 +324,14 @@ enum MockDataGenerator { } // Create the open group model and the thread - let thread: SessionThread = try! SessionThread - .fetchOrCreate( - db, - id: randomGroupPublicKey, - variant: .community, - shouldBeVisible: true - ) + let thread: SessionThread = try! SessionThread.upsert( + db, + id: randomGroupPublicKey, + variant: .community, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: false, + using: dependencies + ) _ = try! OpenGroup( server: serverName, roomToken: roomName, diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index eb46ad1d3ed..c77e40d4167 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -140,7 +140,7 @@ public extension UIContextualAction { Storage.shared.writeAsync { db in try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .deleteContactConversationAndMarkHidden, threadId: threadViewModel.threadId, calledFromConfigHandling: false ) @@ -186,7 +186,7 @@ public extension UIContextualAction { Storage.shared.writeAsync { db in try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .hideContactConversation, threadId: threadViewModel.threadId, calledFromConfigHandling: false ) @@ -343,7 +343,7 @@ public extension UIContextualAction { if threadIsMessageRequest { try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .deleteContactConversationAndMarkHidden, threadId: threadViewModel.threadId, calledFromConfigHandling: false ) @@ -542,8 +542,7 @@ public extension UIContextualAction { case (.group, _), (.legacyGroup, _): return .leaveGroupAsync - case (.contact, _): - return .hideContactConversationAndDeleteContent + case (.contact, _): return .deleteContactConversationAndMarkHidden } }() diff --git a/SessionMessagingKit/Calls/NoopSessionCallManager.swift b/SessionMessagingKit/Calls/NoopSessionCallManager.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift index ae419ce5b44..2b2f33cf6a9 100644 --- a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift @@ -243,8 +243,14 @@ enum _013_SessionUtilChanges: Migration { /// counts so don't do this when running tests (this logic is the same as in `MainAppContext.isRunningTests` if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { if (try SessionThread.exists(db, id: userPublicKey)) == false { - try SessionThread - .fetchOrCreate(db, id: userPublicKey, variant: .contact, shouldBeVisible: false) + try SessionThread.upsert( + db, + id: userPublicKey, + variant: .contact, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), + calledFromConfig: false, + using: dependencies + ) } } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 9dbe1153879..b93bdd2329d 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -148,52 +148,180 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, // MARK: - GRDB Interactions public extension SessionThread { - /// Fetches or creates a SessionThread with the specified id, variant and visible state + /// This type allows the specification of different `SessionThread` properties to use when creating/updating a thread, by default + /// it will attempt to use the values set in `libSession` if none are present + struct TargetValues { + public enum Value { + case setTo(T) + case useLibSession + + /// We should generally try to make `libSession` the source of truth for conversation settings (so they sync between + /// devices) but there are some cases where we don't want to modify a setting (eg. when handling a config change), so + /// this case can be used for those situations + case useExisting + + var valueOrNull: T? { + switch self { + case .setTo(let value): return value + default: return nil + } + } + } + + let creationDateTimestamp: TimeInterval + let shouldBeVisible: Value + let pinnedPriority: Value + let disappearingMessagesConfig: Value + + // MARK: - Convenience + + public static var existingOrDefault: TargetValues { + return TargetValues(shouldBeVisible: .useLibSession) + } + + // MARK: - Initialization + + public init( + creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), + shouldBeVisible: Value, + pinnedPriority: Value = .useLibSession, + disappearingMessagesConfig: Value = .useLibSession + ) { + self.creationDateTimestamp = creationDateTimestamp + self.shouldBeVisible = shouldBeVisible + self.pinnedPriority = pinnedPriority + self.disappearingMessagesConfig = disappearingMessagesConfig + } + } + + /// Updates or inserts a `SessionThread` with the specified `id`, `variant` and specified `values` /// - /// **Notes:** - /// - The `variant` will be ignored if an existing thread is found - /// - This method **will** save the newly created SessionThread to the database - @discardableResult static func fetchOrCreate( + /// **Note:** This method **will** save the newly created/updated `SessionThread` to the database + @discardableResult static func upsert( _ db: Database, id: ID, variant: Variant, - creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), - shouldBeVisible: Bool? + values: TargetValues, + calledFromConfig: Bool, + using dependencies: Dependencies ) throws -> SessionThread { - guard let existingThread: SessionThread = try? fetchOne(db, id: id) else { - return try SessionThread( - id: id, - variant: variant, - creationDateTimestamp: creationDateTimestamp, - shouldBeVisible: (shouldBeVisible ?? false) - ).saved(db) + var result: SessionThread + + /// If the thread doesn't already exist then create it (with the provided defaults) + switch try? fetchOne(db, id: id) { + case .some(let existingThread): result = existingThread + case .none: + let targetPriority: Int32 = LibSession.pinnedPriority( + db, + threadId: id, + threadVariant: variant, + using: dependencies + ) + + result = try SessionThread( + id: id, + variant: variant, + creationDateTimestamp: values.creationDateTimestamp, + shouldBeVisible: LibSession.shouldBeVisible(priority: targetPriority), + pinnedPriority: targetPriority + ).upserted(db) } - // If the `shouldBeVisible` state matches then we can finish early - guard - let desiredVisibility: Bool = shouldBeVisible, - existingThread.shouldBeVisible != desiredVisibility - else { return existingThread } + /// Setup the `DisappearingMessagesConfiguration` as specified + switch (variant, values.disappearingMessagesConfig) { + case (.community, _), (_, .useExisting): break // No need to do anything + case (_, .setTo(let config)): // Save the explicit config + try config + .upserted(db) + .clearUnrelatedControlMessages( + db, + threadVariant: variant + ) + + case (_, .useLibSession): // Create and save the config from libSession + guard !calledFromConfig else { throw LibSessionError.invalidConfigAccess } + + try LibSession + .disappearingMessagesConfig( + db, + threadId: id, + threadVariant: variant, + using: dependencies + )? + .upserted(db) + .clearUnrelatedControlMessages( + db, + threadVariant: variant + ) + } + + /// Apply any changes if the provided `values` don't match the current or default settings + var requiredChanges: [ConfigColumnAssignment] = [] + var finalShouldBeVisible: Bool = result.shouldBeVisible + var finalPinnedPriority: Int32? = result.pinnedPriority + + /// The `shouldBeVisible` flag is based on `pinnedPriority` so we need to check these two together if they + /// should both be sourced from `libSession` + switch (values.pinnedPriority, values.shouldBeVisible) { + case (.useLibSession, .useLibSession): + let targetPriority: Int32 = LibSession.pinnedPriority( + db, + threadId: id, + threadVariant: variant, + using: dependencies + ) + let libSessionShouldBeVisible: Bool = LibSession.shouldBeVisible(priority: targetPriority) + + if targetPriority != result.pinnedPriority { + requiredChanges.append(SessionThread.Columns.pinnedPriority.set(to: targetPriority)) + finalPinnedPriority = targetPriority + } + + if libSessionShouldBeVisible != result.shouldBeVisible { + requiredChanges.append(SessionThread.Columns.shouldBeVisible.set(to: libSessionShouldBeVisible)) + finalShouldBeVisible = libSessionShouldBeVisible + } + + default: break + } + + /// Otherwise we can just handle the explicit `setTo` cases for these + if case .setTo(let value) = values.pinnedPriority, value != result.pinnedPriority { + requiredChanges.append(SessionThread.Columns.pinnedPriority.set(to: value)) + finalPinnedPriority = value + } - // Update the `shouldBeVisible` state + if case .setTo(let value) = values.shouldBeVisible, value != result.shouldBeVisible { + requiredChanges.append(SessionThread.Columns.shouldBeVisible.set(to: value)) + finalShouldBeVisible = value + } + + /// If no changes were needed we can just return the existing/default thread + guard !requiredChanges.isEmpty else { return result } + + /// Otherwise save the changes try SessionThread .filter(id: id) .updateAllAndConfig( db, - SessionThread.Columns.shouldBeVisible.set(to: shouldBeVisible) + calledFromConfig: calledFromConfig, + requiredChanges ) - // Retrieve the updated thread and return it (we don't recursively call this method - // just in case something weird happened and the above update didn't work, as that - // would result in an infinite loop) + /// We need to re-fetch the updated thread as the changes wouldn't have been applied to `result`, it's also possible additional + /// changes could have happened to the thread during the database operations + /// + /// Since we want to avoid returning a nullable `SessionThread` here we need to fallback to a non-null instance, but it should + /// never be called return (try fetchOne(db, id: id)) .defaulting( to: try SessionThread( id: id, variant: variant, - creationDateTimestamp: creationDateTimestamp, - shouldBeVisible: desiredVisibility - ).saved(db) + creationDateTimestamp: values.creationDateTimestamp, + shouldBeVisible: finalShouldBeVisible, + pinnedPriority: finalPinnedPriority + ).upserted(db) ) } @@ -282,7 +410,9 @@ public extension SessionThread { public extension SessionThread { enum DeletionType { - case hideContactConversationAndDeleteContent + case hideContactConversation + case hideContactConversationAndDeleteContentDirectly + case deleteContactConversationAndMarkHidden case deleteContactConversationAndContact case leaveGroupAsync case deleteGroupAndContent @@ -313,21 +443,44 @@ public extension SessionThread { let remainingThreadIds: Set = threadIds.asSet().removing(currentUserPublicKey) switch type { - case .hideContactConversationAndDeleteContent: + case .hideContactConversation: + // We need to custom handle the 'Note to Self' conversation + if threadIds.contains(currentUserPublicKey) { + _ = try SessionThread + .filter(id: currentUserPublicKey) + .updateAllAndConfig( + db, + calledFromConfig: calledFromConfigHandling, + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), + SessionThread.Columns.shouldBeVisible.set(to: false) + ) + } + + // Update any other threads to be hidden + _ = try SessionThread + .filter(ids: remainingThreadIds) + .updateAllAndConfig( + db, + calledFromConfig: calledFromConfigHandling, + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), + SessionThread.Columns.shouldBeVisible.set(to: false) + ) + + case .hideContactConversationAndDeleteContentDirectly: // Clear any interactions for the deleted thread _ = try Interaction .filter(threadIds.contains(Interaction.Columns.threadId)) .deleteAll(db) // We need to custom handle the 'Note to Self' conversation (it should just be - // hidden locally rather than deleted) + // hidden rather than deleted) if threadIds.contains(currentUserPublicKey) { _ = try SessionThread .filter(id: currentUserPublicKey) .updateAllAndConfig( db, calledFromConfig: calledFromConfigHandling, - SessionThread.Columns.pinnedPriority.set(to: 0), + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), SessionThread.Columns.shouldBeVisible.set(to: false) ) } @@ -342,6 +495,34 @@ public extension SessionThread { SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), SessionThread.Columns.shouldBeVisible.set(to: false) ) + + case .deleteContactConversationAndMarkHidden: + _ = try SessionThread + .filter(ids: remainingThreadIds) + .deleteAll(db) + + // We need to custom handle the 'Note to Self' conversation (it should just be + // hidden locally rather than deleted) + if threadIds.contains(currentUserPublicKey) { + // Clear any interactions for the deleted thread + _ = try Interaction + .filter(threadIds.contains(Interaction.Columns.threadId)) + .deleteAll(db) + + _ = try SessionThread + .filter(id: currentUserPublicKey) + .updateAllAndConfig( + db, + calledFromConfig: calledFromConfigHandling, + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), + SessionThread.Columns.shouldBeVisible.set(to: false) + ) + } + + if !calledFromConfigHandling { + // Update any other threads to be hidden + try LibSession.hide(db, contactIds: Array(remainingThreadIds)) + } case .deleteContactConversationAndContact: // If this wasn't called from config handling then we need to hide the conversation diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 331931be3f7..ba5879a7779 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -35,7 +35,8 @@ internal extension LibSession { _ db: Database, in conf: UnsafeMutablePointer?, mergeNeedsDump: Bool, - latestConfigSentTimestampMs: Int64 + latestConfigSentTimestampMs: Int64, + using dependencies: Dependencies ) throws { guard mergeNeedsDump else { return } guard conf != nil else { throw LibSessionError.nilConfigObject } @@ -140,52 +141,48 @@ internal extension LibSession { .fetchOne(db) let threadExists: Bool = (threadInfo != nil) let updatedShouldBeVisible: Bool = LibSession.shouldBeVisible(priority: data.priority) - - /// If we are hiding the conversation then kick the user from it if it's currently open - if !updatedShouldBeVisible { - LibSession.kickFromConversationUIIfNeeded(removedThreadIds: [sessionId]) - } - /// Create the thread if it doesn't exist, otherwise just update it's state - if !threadExists { - try SessionThread( - id: sessionId, - variant: .contact, - creationDateTimestamp: data.created, - shouldBeVisible: updatedShouldBeVisible, - pinnedPriority: data.priority - ).save(db) - } - else { - let changes: [ConfigColumnAssignment] = [ - (threadInfo?.shouldBeVisible == updatedShouldBeVisible ? nil : - SessionThread.Columns.shouldBeVisible.set(to: updatedShouldBeVisible) - ), - (threadInfo?.pinnedPriority == data.priority ? nil : - SessionThread.Columns.pinnedPriority.set(to: data.priority) + switch (updatedShouldBeVisible, threadExists) { + /// If we are hiding the conversation then kick the user from it if it's currently open then delete the thread + case (false, true): + LibSession.kickFromConversationUIIfNeeded(removedThreadIds: [sessionId]) + + try SessionThread.deleteOrLeave( + db, + type: .deleteContactConversationAndMarkHidden, + threadId: sessionId, + calledFromConfigHandling: true ) - ].compactMap { $0 } - try SessionThread - .filter(id: sessionId) - .updateAll( // Handling a config update so don't use `updateAllAndConfig` - db, - changes + /// We need to create or update the thread + case (true, _): + let localConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration + .fetchOne(db, id: sessionId) + .defaulting(to: DisappearingMessagesConfiguration.defaultWith(sessionId)) + let disappearingMessagesConfigChanged: Bool = ( + data.config.isValidV2Config() && + data.config != localConfig ) - } - - // Update disappearing messages configuration if needed - let localConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration - .fetchOne(db, id: sessionId) - .defaulting(to: DisappearingMessagesConfiguration.defaultWith(sessionId)) - - if data.config.isValidV2Config() && data.config != localConfig { - try data.config - .saved(db) - .clearUnrelatedControlMessages( + + _ = try SessionThread.upsert( db, - threadVariant: .contact + id: sessionId, + variant: .contact, + values: SessionThread.TargetValues( + creationDateTimestamp: data.created, + shouldBeVisible: .setTo(updatedShouldBeVisible), + pinnedPriority: .setTo(data.priority), + disappearingMessagesConfig: (disappearingMessagesConfigChanged ? + .setTo(data.config) : + .useExisting + ) + ), + calledFromConfig: true, + using: dependencies ) + + /// Thread shouldn't be visible and doesn't exist so no need to do anything + case (false, false): break } } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 5d06cc6eac1..80f858686bf 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -13,6 +13,9 @@ public extension LibSession { enum Crypto { public typealias Domain = String } + + /// The default priority for newly created threads + static var defaultNewThreadPriority: Int32 { return visiblePriority } /// A `0` `priority` value indicates visible, but not pinned static let visiblePriority: Int32 = 0 @@ -389,6 +392,158 @@ internal extension LibSession { // MARK: - External Outgoing Changes public extension LibSession { + static func pinnedPriority( + _ db: Database, + threadId: String, + threadVariant: SessionThread.Variant, + using dependencies: Dependencies + ) -> Int32 { + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let configVariant: ConfigDump.Variant = { + switch threadVariant { + case .contact: return (threadId == userPublicKey ? .userProfile : .contacts) + case .legacyGroup, .group, .community: return .userGroups + } + }() + + return dependencies.caches[.libSession] + .config(for: configVariant, publicKey: userPublicKey) + .wrappedValue + .map { conf in + guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { + return LibSession.defaultNewThreadPriority + } + + switch threadVariant { + case .contact where threadId == userPublicKey: + return user_profile_get_nts_priority(conf) + + case .contact: + var contact: contacts_contact = contacts_contact() + + guard contacts_get(conf, &contact, &cThreadId) else { + LibSessionError.clear(conf) + return LibSession.defaultNewThreadPriority + } + + return contact.priority + + case .community: + let maybeUrlInfo: OpenGroupUrlInfo? = Storage.shared + .read { db in try OpenGroupUrlInfo.fetchAll(db, ids: [threadId]) }? + .first + + guard + let urlInfo: OpenGroupUrlInfo = maybeUrlInfo, + var cBaseUrl: [CChar] = urlInfo.server.cString(using: .utf8), + var cRoom: [CChar] = urlInfo.roomToken.cString(using: .utf8) + else { return LibSession.defaultNewThreadPriority } + + var community: ugroups_community_info = ugroups_community_info() + let result: Bool = user_groups_get_community(conf, &community, &cBaseUrl, &cRoom) + LibSessionError.clear(conf) + + return community.priority + + case .legacyGroup: + let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) + LibSessionError.clear(conf) + + defer { + if groupInfo != nil { + ugroups_legacy_group_free(groupInfo) + } + } + + return (groupInfo?.pointee.priority ?? LibSession.defaultNewThreadPriority) + + case .group: + return LibSession.defaultNewThreadPriority // FIXME: Add in groups rebuild + } + } + .defaulting(to: LibSession.defaultNewThreadPriority) + } + + static func disappearingMessagesConfig( + _ db: Database, + threadId: String, + threadVariant: SessionThread.Variant, + using dependencies: Dependencies + ) throws -> DisappearingMessagesConfiguration? { + switch threadVariant { + case .community: return nil + default: break + } + + let userPublicKey: String = getUserHexEncodedPublicKey(db) + let configVariant: ConfigDump.Variant = { + switch threadVariant { + case .contact: return (threadId == userPublicKey ? .userProfile : .contacts) + case .legacyGroup, .group, .community: return .userGroups + } + }() + + return dependencies.caches[.libSession] + .config(for: configVariant, publicKey: userPublicKey) + .wrappedValue + .map { conf -> DisappearingMessagesConfiguration? in + guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { return nil } + + switch threadVariant { + case .community: return nil + case .contact where threadId == userPublicKey: + let targetExpiry: Int32 = user_profile_get_nts_expiry(conf) + let targetIsEnabled: Bool = (targetExpiry > 0) + + return DisappearingMessagesConfiguration( + threadId: threadId, + isEnabled: targetIsEnabled, + durationSeconds: TimeInterval(targetExpiry), + type: targetIsEnabled ? .disappearAfterSend : .unknown + ) + + case .contact: + var contact: contacts_contact = contacts_contact() + + guard contacts_get(conf, &contact, &cThreadId) else { + LibSessionError.clear(conf) + return nil + } + + return DisappearingMessagesConfiguration( + threadId: threadId, + isEnabled: contact.exp_seconds > 0, + durationSeconds: TimeInterval(contact.exp_seconds), + type: DisappearingMessagesConfiguration.DisappearingMessageType( + libSessionType: contact.exp_mode + ) + ) + + case .legacyGroup: + let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) + LibSessionError.clear(conf) + + defer { + if groupInfo != nil { + ugroups_legacy_group_free(groupInfo) + } + } + + return groupInfo.map { info in + DisappearingMessagesConfiguration( + threadId: userPublicKey, + isEnabled: (info.pointee.disappearing_timer > 0), + durationSeconds: TimeInterval(info.pointee.disappearing_timer), + type: .disappearAfterSend + ) + } + + case .group: + return nil // FIXME: Add in groups rebuild + } + } + } + static func conversationInConfig( _ db: Database? = nil, threadId: String, diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 44b1f7d239d..049e697ad10 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -86,32 +86,29 @@ internal extension LibSession { } } else { - try SessionThread - .fetchOrCreate( - db, - id: userPublicKey, - variant: .contact, - shouldBeVisible: LibSession.shouldBeVisible(priority: targetPriority) - ) - - try SessionThread - .filter(id: userPublicKey) - .updateAll( // Handling a config update so don't use `updateAllAndConfig` - db, - SessionThread.Columns.pinnedPriority.set(to: targetPriority) - ) - // If the 'Note to Self' conversation is hidden then we should trigger the proper - // `deleteOrLeave` behaviour (for 'Note to Self' this will leave the conversation - // but remove the associated interactions) + // `deleteOrLeave` behaviour if !LibSession.shouldBeVisible(priority: targetPriority) { try SessionThread.deleteOrLeave( db, - type: .hideContactConversationAndDeleteContent, + type: .hideContactConversation, threadId: userPublicKey, calledFromConfigHandling: true ) } + else { + try SessionThread.upsert( + db, + id: userPublicKey, + variant: .contact, + values: SessionThread.TargetValues( + shouldBeVisible: .setTo(LibSession.shouldBeVisible(priority: targetPriority)), + pinnedPriority: .setTo(targetPriority) + ), + calledFromConfig: true, + using: dependencies + ) + } } // Update the 'Note to Self' disappearing messages configuration @@ -213,6 +210,28 @@ internal extension LibSession { } } +// MARK: - External Outgoing Changes + +public extension LibSession { + static func updateNoteToSelf( + _ db: Database, + priority: Int32? = nil, + disappearingMessagesConfig: DisappearingMessagesConfiguration? = nil + ) throws { + try LibSession.performAndPushChange( + db, + for: .userProfile, + publicKey: getUserHexEncodedPublicKey(db) + ) { conf in + try LibSession.updateNoteToSelf( + priority: priority, + disappearingMessagesConfig: disappearingMessagesConfig, + in: conf + ) + } + } +} + // MARK: - Direct Values extension LibSession { diff --git a/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift b/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift index eafcd4898c6..303a1c65b18 100644 --- a/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift +++ b/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift @@ -97,7 +97,7 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table // Then check if any of the changes could affect the config guard !calledFromConfig && - LibSession.assignmentsRequireConfigUpdate(assignments) + LibSession.assignmentsRequireConfigUpdate(assignments) else { return updatedData } defer { diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 16a2bad9e4f..b1bb6e54b31 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -457,7 +457,8 @@ public extension LibSession { db, in: conf, mergeNeedsDump: config_needs_dump(conf), - latestConfigSentTimestampMs: latestConfigSentTimestampMs + latestConfigSentTimestampMs: latestConfigSentTimestampMs, + using: dependencies ) case .convoInfoVolatile: diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 0b2aadae4d4..e6a61249bc0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -201,18 +201,21 @@ public final class OpenGroupManager { // Optionally try to insert a new version of the OpenGroup (it will fail if there is already an // inactive one but that won't matter as we then activate it) - _ = try? SessionThread - .fetchOrCreate( - db, - id: threadId, - variant: .community, + _ = try? SessionThread.upsert( + db, + id: threadId, + variant: .community, + values: SessionThread.TargetValues( /// If we didn't add this open group via config handling then flag it to be visible (if it did come via config handling then /// we want to wait until it actually has messages before making it visible) /// /// **Note:** We **MUST** provide a `nil` value if this method was called from the config handling as updating /// the `shouldVeVisible` state can trigger a config update which could result in an infinite loop in the future - shouldBeVisible: (calledFromConfigHandling ? nil : true) - ) + shouldBeVisible: (calledFromConfigHandling ? .useExisting : .setTo(true)) + ), + calledFromConfig: calledFromConfigHandling, + using: dependencies + ) if (try? OpenGroup.exists(db, id: threadId)) == false { try? OpenGroup diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 127ea94ac28..8784ddb4b53 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -60,8 +60,14 @@ extension MessageReceiver { guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestampMs: timestamp) else { // Add missed call message for call offer messages from more than one minute if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .missed, using: dependencies) { - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: sender, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) if !interaction.wasRead { SessionEnvironment.shared?.notificationsManager.wrappedValue? @@ -81,8 +87,14 @@ extension MessageReceiver { let state: CallMessage.MessageInfo.State = (db[.areCallsEnabled] ? .permissionDeniedMicrophone : .permissionDenied) if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: state, using: dependencies) { - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: sender, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) if !interaction.wasRead { SessionEnvironment.shared?.notificationsManager.wrappedValue? diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 2ecf4aa9406..68515f84d37 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -152,14 +152,17 @@ extension MessageReceiver { guard hasApprovedAdmin || calledFromConfigHandling else { return } // Create the group - let thread: SessionThread = try SessionThread - .fetchOrCreate( - db, - id: groupPublicKey, - variant: .legacyGroup, + let thread: SessionThread = try SessionThread.upsert( + db, + id: groupPublicKey, + variant: .legacyGroup, + values: SessionThread.TargetValues( creationDateTimestamp: (TimeInterval(formationTimestampMs) / 1000), - shouldBeVisible: true - ) + shouldBeVisible: .setTo(true) + ), + calledFromConfig: false, + using: dependencies + ) let closedGroup: ClosedGroup = try ClosedGroup( threadId: groupPublicKey, name: name, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index c9d240fbfa6..ca0cbd40f14 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -50,8 +50,14 @@ extension MessageReceiver { } // Prep the unblinded thread - let unblindedThread: SessionThread = try SessionThread - .fetchOrCreate(db, id: senderId, variant: .contact, shouldBeVisible: nil) + let unblindedThread: SessionThread = try SessionThread.upsert( + db, + id: senderId, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) // Need to handle a `MessageRequestResponse` sent to a blinded thread (ie. check if the sender matches // the blinded ids of any threads) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 067a6a21384..126d39e91dd 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -67,8 +67,14 @@ extension MessageReceiver { // Store the message variant so we can run variant-specific behaviours let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: threadId, variant: threadVariant, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: threadId, + variant: threadVariant, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) let maybeOpenGroup: OpenGroup? = { guard threadVariant == .community else { return nil } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index d89d221aa1d..0da981c1a2b 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -40,8 +40,14 @@ extension MessageSender { let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) // Create the relevant objects in the database - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true) + let thread: SessionThread = try SessionThread.upsert( + db, + id: groupPublicKey, + variant: .legacyGroup, + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: false, + using: dependencies + ) try ClosedGroup( threadId: groupPublicKey, name: name, @@ -468,7 +474,14 @@ extension MessageSender { try addedMembers.forEach { member in // Send updates to the new members individually - try SessionThread.fetchOrCreate(db, id: member, variant: .contact, shouldBeVisible: nil) + try SessionThread.upsert( + db, + id: member, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) try MessageSender.send( db, @@ -675,8 +688,14 @@ extension MessageSender { privateKey: keyPair.secretKey ).build() let plaintext = try proto.serializedData() - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: publicKey, variant: .contact, shouldBeVisible: nil) + let thread: SessionThread = try SessionThread.upsert( + db, + id: publicKey, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) let ciphertext = try dependencies.crypto.tryGenerate( .ciphertextWithSessionProtocol( db, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index ebd62cc1cd8..5d27f601f67 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -340,7 +340,8 @@ public enum PushNotificationAPI { receiveCompletion: { result in switch result { case .finished: break - case .failure: Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups.") + case .failure(let error): + Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups due to error: \(error).") } } ) diff --git a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift index e2ab1137841..9a02e261e2b 100644 --- a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift @@ -63,11 +63,13 @@ class MessageSendJobSpec: QuickSpec { SNMessagingKit.self ], initialData: { db in - try SessionThread.fetchOrCreate( + try SessionThread.upsert( db, id: "Test1", variant: .contact, - shouldBeVisible: true + values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + calledFromConfig: false, + using: dependencies ) }, using: dependencies diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 2ee25b6b184..742a82be716 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -164,13 +164,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension using: dependencies ) { - let thread: SessionThread = try SessionThread - .fetchOrCreate( - db, - id: sender, - variant: .contact, - shouldBeVisible: nil - ) + let thread: SessionThread = try SessionThread.upsert( + db, + id: sender, + variant: .contact, + values: .existingOrDefault, + calledFromConfig: false, + using: dependencies + ) // Notify the user if the call message wasn't already read if !interaction.wasRead { diff --git a/SessionUtilitiesKit/LibSession/LibSessionError.swift b/SessionUtilitiesKit/LibSession/LibSessionError.swift index bf263c0c44b..4c879c7d923 100644 --- a/SessionUtilitiesKit/LibSession/LibSessionError.swift +++ b/SessionUtilitiesKit/LibSession/LibSessionError.swift @@ -13,6 +13,7 @@ public enum LibSessionError: Error, CustomStringConvertible { case processingLoopLimitReached case invalidCConversion case unableToGeneratePushData + case invalidConfigAccess case libSessionError(String) case unknown @@ -70,6 +71,7 @@ public enum LibSessionError: Error, CustomStringConvertible { case .processingLoopLimitReached: return "Processing loop limit reached (LibSessionError.processingLoopLimitReached)." case .invalidCConversion: return "Invalid conversation to C type (LibSessionError.invalidCConversion)." case .unableToGeneratePushData: return "Unable to generate push data (LibSessionError.unableToGeneratePushData)." + case .invalidConfigAccess: return "Invalid config access (LibSessionError.invalidConfigAccess)." case .libSessionError(let error): return "\(error)\(error.hasSuffix(".") ? "" : ".")" case .unknown: return "An unknown error occurred (LibSessionError.unknown)." From e1c5215986ed6ac8500ccc5b9d242a4728743d10 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Nov 2024 09:44:03 +1100 Subject: [PATCH 05/33] Various dependency changes required to get unit tests working correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated the SessionCallManager to be an updated singleton type (cleaned up more in Groups Rebuild) • Updated the PushRegistrationManager to be an updated singleton type (cleaned up more in Groups Rebuild) • Injected dependencies correctly in a bunch of places --- Session.xcodeproj/project.pbxproj | 8 + .../Calls/Call Management/SessionCall.swift | 24 +- .../SessionCallManager+CXCallController.swift | 22 +- .../Call Management/SessionCallManager.swift | 124 +++++--- Session/Calls/CallVC.swift | 8 +- .../Views & Modals/IncomingCallBanner.swift | 4 +- .../ConversationVC+Interaction.swift | 42 ++- .../Conversations/ConversationViewModel.swift | 16 +- ...isappearingMessagesSettingsViewModel.swift | 26 +- .../Settings/ThreadSettingsViewModel.swift | 11 +- Session/Home/HomeVC.swift | 9 +- .../MessageRequestsViewModel.swift | 9 +- Session/Meta/AppDelegate.swift | 22 +- Session/Meta/AppEnvironment.swift | 7 - Session/Notifications/AppNotifications.swift | 12 +- .../PushRegistrationManager.swift | 67 ++-- .../PushRegistrationManagerType.swift | 29 ++ Session/Notifications/SyncPushTokensJob.swift | 2 +- .../UserNotificationsAdaptee.swift | 2 +- Session/Onboarding/DisplayNameScreen.swift | 2 +- Session/Onboarding/LoadingScreen.swift | 2 +- Session/Onboarding/Onboarding.swift | 11 +- Session/Onboarding/PNModeScreen.swift | 2 +- .../Settings/BlockedContactsViewModel.swift | 8 +- .../Settings/PrivacySettingsViewModel.swift | 33 +- .../UIContextualAction+Utilities.swift | 32 +- .../Calls/CallManagerProtocol.swift | 22 ++ .../Calls/CurrentCallProtocol.swift | 3 +- .../Calls/NoopSessionCallManager.swift | 25 ++ .../_014_GenerateInitialUserConfigDumps.swift | 2 +- ...18_DisappearingMessagesConfiguration.swift | 2 +- .../Database/Models/BlindedIdLookup.swift | 6 +- .../Database/Models/ClosedGroup.swift | 17 +- .../Database/Models/Interaction.swift | 18 +- .../Database/Models/SessionThread.swift | 44 ++- .../Jobs/Types/GroupLeavingJob.swift | 3 +- .../Config Handling/LibSession+Contacts.swift | 72 +++-- .../LibSession+ConvoInfoVolatile.swift | 45 ++- .../Config Handling/LibSession+Shared.swift | 36 ++- .../LibSession+UserGroups.swift | 59 ++-- .../LibSession+UserProfile.swift | 9 +- .../QueryInterfaceRequest+Utilities.swift | 39 ++- .../Database/Setting+Utilities.swift | 96 +++++- .../LibSession+SessionMessagingKit.swift | 5 +- .../Open Groups/OpenGroupManager.swift | 18 +- .../MessageReceiver+Calls.swift | 34 +- .../MessageReceiver+ClosedGroups.swift | 48 ++- .../MessageReceiver+ExpirationTimers.swift | 12 +- .../MessageReceiver+MessageRequests.swift | 24 +- .../MessageReceiver+UnsendRequests.swift | 6 +- .../MessageReceiver+VisibleMessages.swift | 17 +- .../MessageSender+ClosedGroups.swift | 12 +- .../Sending & Receiving/MessageReceiver.swift | 26 +- .../SessionThreadViewModel.swift | 13 +- .../Utilities/ProfileManager.swift | 2 +- .../Utilities/SessionEnvironment.swift | 3 - .../Jobs/Types/MessageSendJobSpec.swift | 7 +- .../Open Groups/OpenGroupManagerSpec.swift | 292 ++++++++++-------- .../NotificationServiceExtension.swift | 6 +- SessionShareExtension/ThreadPickerVC.swift | 3 +- 60 files changed, 1034 insertions(+), 526 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2e50a323b87..783bb078d3e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -685,6 +685,8 @@ FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */; }; FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; }; FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; + FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; + FD6C67262CF6EA2300B350A7 /* PushRegistrationManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67252CF6EA1E00B350A7 /* PushRegistrationManagerType.swift */; }; FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; @@ -1832,6 +1834,8 @@ FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = ""; }; FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; + FD6C67252CF6EA1E00B350A7 /* PushRegistrationManagerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRegistrationManagerType.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; @@ -2783,6 +2787,7 @@ FD716E692850327900C96BF4 /* EndCallMode.swift */, FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */, FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */, + FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */, ); path = Calls; sourceTree = ""; @@ -3177,6 +3182,7 @@ children = ( FDC498B52AC15F6D00EDD897 /* Types */, 4539B5851F79348F007141FF /* PushRegistrationManager.swift */, + FD6C67252CF6EA1E00B350A7 /* PushRegistrationManagerType.swift */, 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */, 451A13B01E13DED2000A50FD /* AppNotifications.swift */, 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */, @@ -5929,6 +5935,7 @@ FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, + FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */, FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */, @@ -6226,6 +6233,7 @@ FDEF57242C3CF04700131302 /* WebRTC+Utilities.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */, + FD6C67262CF6EA2300B350A7 /* PushRegistrationManagerType.swift in Sources */, B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 010f9c78a45..ab07e20e66a 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -15,6 +15,8 @@ import SessionSnodeKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { @objc static let isEnabled = true + private let dependencies: Dependencies + // MARK: - Metadata Properties public let uuid: String public let callId: UUID // This is for CallKit @@ -147,7 +149,8 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // MARK: - Initialization - init(_ db: Database, for sessionId: String, uuid: String, mode: CallMode, outgoing: Bool = false) { + init(_ db: Database, for sessionId: String, uuid: String, mode: CallMode, outgoing: Bool = false, using dependencies: Dependencies) { + self.dependencies = dependencies self.sessionId = sessionId self.uuid = uuid self.callId = UUID() @@ -172,8 +175,8 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { WebRTCSession.current = self.webRTCSession self.webRTCSession.delegate = self - if AppEnvironment.shared.callManager.currentCall == nil { - AppEnvironment.shared.callManager.currentCall = self + if Singleton.callManager.currentCall == nil { + Singleton.callManager.setCurrentCall(self) } else { SNLog("[Calls] A call is ongoing.") @@ -183,12 +186,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // stringlint:ignore_contents func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) { guard case .answer = mode else { - SessionCallManager.reportFakeCall(info: "Call not in answer mode") + SessionCallManager.reportFakeCall(info: "Call not in answer mode", using: dependencies) return } setupTimeoutTimer() - AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in + Singleton.callManager.reportIncomingCall(self, callerName: contactName) { error in completion(error) } } @@ -290,7 +293,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // MARK: - Call Message Handling - public func updateCallMessage(mode: EndCallMode) { + public func updateCallMessage(mode: EndCallMode, using dependencies: Dependencies) { guard let callInteractionId: Int64 = callInteractionId else { return } let duration: TimeInterval = self.duration @@ -351,11 +354,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { threadId: interaction.threadId, threadVariant: threadVariant, includingOlder: false, - trySendReadReceipt: false + trySendReadReceipt: false, + using: dependencies ) }, completion: { _, _ in - SessionCallManager.suspendDatabaseIfCallEndedInBackground() + Singleton.callManager.suspendDatabaseIfCallEndedInBackground() } ) } @@ -404,7 +408,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { guard Singleton.hasAppContext else { return } if let callVC = Singleton.appContext.frontmostViewController as? CallVC { callVC.handleEndCallMessage() } if let miniCallView = MiniCallView.current { miniCallView.dismiss() } - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: .remoteEnded) + Singleton.callManager.reportCurrentCallEnded(reason: .remoteEnded) } } @@ -461,7 +465,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { timeOutTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: timeInterval, repeats: false) { _ in self.didTimeout = true - AppEnvironment.shared.callManager.endCall(self) { error in + Singleton.callManager.endCall(self) { error in self.timeOutTimer = nil } } diff --git a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift index 1824a9d5c16..ad078c5251b 100644 --- a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift +++ b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift @@ -2,12 +2,16 @@ import Foundation import CallKit +import SessionMessagingKit import SessionUtilitiesKit extension SessionCallManager { - public func startCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - guard case .offer = call.mode else { return } - guard !call.hasConnected else { return } + public func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + guard + let call: SessionCall = call as? SessionCall, + case .offer = call.mode, + !call.hasConnected + else { return } reportOutgoingCall(call) @@ -28,9 +32,9 @@ extension SessionCallManager { } } - public func answerCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - if callController != nil { - let answerCallAction = CXAnswerCallAction(call: call.callId) + public func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + if callController != nil, let callId: UUID = call?.callId { + let answerCallAction = CXAnswerCallAction(call: callId) let transaction = CXTransaction() transaction.addAction(answerCallAction) @@ -42,9 +46,9 @@ extension SessionCallManager { } } - public func endCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - if callController != nil { - let endCallAction = CXEndCallAction(call: call.callId) + public func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + if callController != nil, let callId: UUID = call?.callId { + let endCallAction = CXEndCallAction(call: callId) let transaction = CXTransaction() transaction.addAction(endCallAction) diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 7235ec02aed..e339fc79e59 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -9,7 +9,21 @@ import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit +// MARK: - Cache + +public extension Cache { + static let callManager: CacheInfo.Config = CacheInfo.create( + createInstance: { SessionCallManager.Cache() }, + mutableInstance: { $0 }, + immutableInstance: { $0 } + ) +} + +// MARK: - SessionCallManager + public final class SessionCallManager: NSObject, CallManagerProtocol { + let dependencies: Dependencies + let provider: CXProvider? let callController: CXCallController? @@ -27,40 +41,15 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } - private static var _sharedProvider: CXProvider? - static func sharedProvider(useSystemCallLog: Bool) -> CXProvider { - let configuration = buildProviderConfiguration(useSystemCallLog: useSystemCallLog) - - if let sharedProvider = self._sharedProvider { - sharedProvider.configuration = configuration - return sharedProvider - } - else { - SwiftSingletons.register(self) - let provider = CXProvider(configuration: configuration) - _sharedProvider = provider - return provider - } - } - - static func buildProviderConfiguration(useSystemCallLog: Bool) -> CXProviderConfiguration { - let providerConfiguration = CXProviderConfiguration() - providerConfiguration.supportsVideo = true - providerConfiguration.maximumCallGroups = 1 - providerConfiguration.maximumCallsPerCallGroup = 1 - providerConfiguration.supportedHandleTypes = [.generic] - let iconMaskImage = #imageLiteral(resourceName: "SessionGreen32") - providerConfiguration.iconTemplateImageData = iconMaskImage.pngData() - providerConfiguration.includesCallsInRecents = useSystemCallLog - - return providerConfiguration - } - // MARK: - Initialization - init(useSystemCallLog: Bool = false) { + init(useSystemCallLog: Bool = false, using dependencies: Dependencies) { + self.dependencies = dependencies + if Preferences.isCallKitSupported { - self.provider = SessionCallManager.sharedProvider(useSystemCallLog: useSystemCallLog) + self.provider = dependencies.caches.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: useSystemCallLog) + } self.callController = CXCallController() } else { @@ -76,9 +65,11 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - Report calls - public static func reportFakeCall(info: String) { + public static func reportFakeCall(info: String, using dependencies: Dependencies) { let callId = UUID() - let provider = SessionCallManager.sharedProvider(useSystemCallLog: false) + let provider: CXProvider = dependencies.caches.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: false) + } provider.reportNewIncomingCall( with: callId, update: CXCallUpdate() @@ -92,6 +83,10 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { ) } + public func setCurrentCall(_ call: CurrentCallProtocol?) { + self.currentCall = call + } + public func reportOutgoingCall(_ call: SessionCall) { Log.assertOnMainThread() UserDefaults.sharedLokiProject?[.isCallOngoing] = true @@ -108,8 +103,10 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } - public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) { - let provider = provider ?? Self.sharedProvider(useSystemCallLog: false) + public func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) { + let provider: CXProvider = dependencies.caches.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: false) + } // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.localizedCallerName = callerName @@ -152,7 +149,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { guard let call = currentCall else { handleCallEnded() - Self.suspendDatabaseIfCallEndedInBackground() + suspendDatabaseIfCallEndedInBackground() return } @@ -160,14 +157,14 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { self.provider?.reportCall(with: call.callId, endedAt: nil, reason: reason) switch (reason) { - case .answeredElsewhere: call.updateCallMessage(mode: .answeredElsewhere) - case .unanswered: call.updateCallMessage(mode: .unanswered) - case .declinedElsewhere: call.updateCallMessage(mode: .local) - default: call.updateCallMessage(mode: .remote) + case .answeredElsewhere: call.updateCallMessage(mode: .answeredElsewhere, using: dependencies) + case .unanswered: call.updateCallMessage(mode: .unanswered, using: dependencies) + case .declinedElsewhere: call.updateCallMessage(mode: .local, using: dependencies) + default: call.updateCallMessage(mode: .remote, using: dependencies) } } else { - call.updateCallMessage(mode: .local) + call.updateCallMessage(mode: .local, using: dependencies) } (call as? SessionCall)?.webRTCSession.dropConnection() @@ -197,7 +194,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { callUpdate.supportsDTMF = false } - public static func suspendDatabaseIfCallEndedInBackground() { + public func suspendDatabaseIfCallEndedInBackground() { if Singleton.hasAppContext && Singleton.appContext.isInBackground { // FIXME: Initialise the `SessionCallManager` with a dependencies instance let dependencies: Dependencies = Dependencies() @@ -214,9 +211,11 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - UI public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) { - guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else { - return - } + guard + let call: SessionCall = Storage.shared.read({ [dependencies] db in + SessionCall(db, for: caller, uuid: uuid, mode: mode, using: dependencies) + }) + else { return } call.callInteractionId = interactionId call.reportIncomingCallIfNeeded { error in @@ -295,3 +294,38 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } +// MARK: - SessionCallManager Cache + +public extension SessionCallManager { + class Cache: CallManagerCacheType { + public var provider: CXProvider? + + public func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider { + if let provider: CXProvider = self.provider { + return provider + } + + let iconMaskImage: UIImage = #imageLiteral(resourceName: "SessionGreen32") + let configuration = CXProviderConfiguration() + configuration.supportsVideo = true + configuration.maximumCallGroups = 1 + configuration.maximumCallsPerCallGroup = 1 + configuration.supportedHandleTypes = [.generic] + configuration.iconTemplateImageData = iconMaskImage.pngData() + configuration.includesCallsInRecents = useSystemCallLog + + let provider: CXProvider = CXProvider(configuration: configuration) + self.provider = provider + return provider + } + } +} + +// MARK: - OGMCacheType + +/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way +public protocol CallManagerImmutableCacheType: ImmutableCacheType {} + +public protocol CallManagerCacheType: CallManagerImmutableCacheType, MutableCacheType { + func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider +} diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index b6b35276757..491b548b70e 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -429,7 +429,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { _ = call.videoCapturer // Force the lazy var to instantiate titleLabel.text = self.call.contactName - AppEnvironment.shared.callManager.startCall(call) { [weak self] error in + Singleton.callManager.startCall(call) { [weak self] error in DispatchQueue.main.async { if let _ = error { self?.callInfoLabel.text = "callsErrorStart".localized() @@ -606,7 +606,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } @objc private func answerCall() { - AppEnvironment.shared.callManager.answerCall(call) { [weak self] error in + Singleton.callManager.answerCall(call) { [weak self] error in DispatchQueue.main.async { if let _ = error { self?.callInfoLabel.text = "callsErrorAnswer".localized() @@ -617,10 +617,10 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } @objc private func endCall() { - AppEnvironment.shared.callManager.endCall(call) { [weak self] error in + Singleton.callManager.endCall(call) { [weak self] error in if let _ = error { self?.call.endSessionCall() - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) + Singleton.callManager.reportCurrentCallEnded(reason: nil) } DispatchQueue.main.async { diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 5e0f7850e79..14738a5e7a1 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -184,10 +184,10 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { } @objc private func endCall() { - AppEnvironment.shared.callManager.endCall(call) { error in + Singleton.callManager.endCall(call) { error in if let _ = error { self.call.endSessionCall() - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) + Singleton.callManager.reportCurrentCallEnded(reason: nil) } self.dismiss() diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 0c0630d3d56..2f1511f56e0 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -120,12 +120,21 @@ extension ConversationVC: let threadId: String = self.viewModel.threadData.threadId - guard AVAudioSession.sharedInstance().recordPermission == .granted else { return } - guard self.viewModel.threadData.threadVariant == .contact else { return } - guard AppEnvironment.shared.callManager.currentCall == nil else { return } - guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: threadId, uuid: UUID().uuidString.lowercased(), mode: .offer, outgoing: true) }) else { - return - } + guard + AVAudioSession.sharedInstance().recordPermission == .granted, + self.viewModel.threadData.threadVariant == .contact, + Singleton.callManager.currentCall == nil, + let call: SessionCall = Storage.shared.read({ [dependencies = viewModel.dependencies] db in + SessionCall( + db, + for: threadId, + uuid: UUID().uuidString.lowercased(), + mode: .offer, + outgoing: true, + using: dependencies + ) + }) + else { return } let callVC = CallVC(for: call) callVC.conversationVC = self @@ -540,7 +549,8 @@ extension ConversationVC: .updateAllAndConfig( db, SessionThread.Columns.shouldBeVisible.set(to: true), - SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority), + using: dependencies ) } @@ -958,7 +968,8 @@ extension ConversationVC: .update( db, sessionId: cellViewModel.threadId, - disappearingMessagesConfig: messageDisappearingConfig + disappearingMessagesConfig: messageDisappearingConfig, + using: dependencies ) } self?.dismiss(animated: true, completion: nil) @@ -1517,7 +1528,11 @@ extension ConversationVC: if self?.viewModel.threadData.threadShouldBeVisible == false { _ = try SessionThread .filter(id: cellViewModel.threadId) - .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true)) + .updateAllAndConfig( + db, + SessionThread.Columns.shouldBeVisible.set(to: true), + using: dependencies + ) } let pendingReaction: Reaction? = { @@ -2721,7 +2736,8 @@ extension ConversationVC { db, Contact.Columns.isApproved.set(to: true), Contact.Columns.didApproveMe - .set(to: contact.didApproveMe || !isNewThread) + .set(to: contact.didApproveMe || !isNewThread), + using: dependencies ) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) @@ -2751,7 +2767,8 @@ extension ConversationVC { tableView: self.tableView, threadViewModel: self.viewModel.threadData, viewController: self, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: viewModel.dependencies ) guard let action: UIContextualAction = actions?.first else { return } @@ -2775,7 +2792,8 @@ extension ConversationVC { tableView: self.tableView, threadViewModel: self.viewModel.threadData, viewController: self, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: viewModel.dependencies ) guard let action: UIContextualAction = actions?.first else { return } diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 3d89d4c11f5..2e99200bde1 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -782,18 +782,18 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold markAsReadPublisher = markAsReadTrigger .throttle(for: .milliseconds(100), scheduler: DispatchQueue.global(qos: .userInitiated), latest: true) .handleEvents( - receiveOutput: { [weak self] target, timestampMs in + receiveOutput: { [weak self, dependencies] target, timestampMs in let threadData: SessionThreadViewModel? = self?._threadData.wrappedValue switch target { - case .thread: threadData?.markAsRead(target: target) + case .thread: threadData?.markAsRead(target: target, using: dependencies) case .threadAndInteractions(let interactionId): guard timestampMs == nil || (self?.lastInteractionTimestampMsMarkedAsRead ?? 0) < (timestampMs ?? 0) || (self?.lastInteractionIdMarkedAsRead ?? 0) < (interactionId ?? 0) else { - threadData?.markAsRead(target: .thread) + threadData?.markAsRead(target: .thread, using: dependencies) return } @@ -804,7 +804,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold } self?.lastInteractionIdMarkedAsRead = (interactionId ?? threadData?.interactionId) - threadData?.markAsRead(target: target) + threadData?.markAsRead(target: target, using: dependencies) } } ) @@ -872,10 +872,14 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold let threadId: String = self.threadId let displayName: String = self._threadData.wrappedValue.displayName - Storage.shared.writeAsync { db in + Storage.shared.writeAsync { [dependencies] db in try Contact .filter(id: threadId) - .updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false)) + .updateAllAndConfig( + db, + Contact.Columns.isBlocked.set(to: false), + using: dependencies + ) } } diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift index 74cc4b82b92..26abc763278 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift @@ -375,23 +375,23 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga } // Contacts & legacy closed groups need to update the LibSession - dependencies.storage.writeAsync(using: dependencies) { [threadId, threadVariant] db in + dependencies.storage.writeAsync(using: dependencies) { [threadId, threadVariant, dependencies] db in switch threadVariant { case .contact: - try LibSession - .update( - db, - sessionId: threadId, - disappearingMessagesConfig: updatedConfig - ) + try LibSession.update( + db, + sessionId: threadId, + disappearingMessagesConfig: updatedConfig, + using: dependencies + ) case .legacyGroup: - try LibSession - .update( - db, - groupPublicKey: threadId, - disappearingConfig: updatedConfig - ) + try LibSession.update( + db, + groupPublicKey: threadId, + disappearingConfig: updatedConfig, + using: dependencies + ) default: break } diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 4e04bbcc5d1..c95caee7d61 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -157,7 +157,8 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi .updateAllAndConfig( db, Profile.Columns.nickname - .set(to: (updatedNickname.isEmpty ? nil : editedDisplayName)) + .set(to: (updatedNickname.isEmpty ? nil : editedDisplayName)), + using: dependencies ) } } @@ -537,7 +538,8 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi db, type: .leaveGroupAsync, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -837,12 +839,13 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi ) { guard oldBlockedState != isBlocked else { return } - dependencies.storage.writeAsync { db in + dependencies.storage.writeAsync { [dependencies] db in try Contact .filter(id: threadId) .updateAllAndConfig( db, - Contact.Columns.isBlocked.set(to: isBlocked) + Contact.Columns.isBlocked.set(to: isBlocked), + using: dependencies ) } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 7335d3b7da4..a4c5821c3e3 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -751,7 +751,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) @@ -773,7 +774,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) @@ -820,7 +822,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index e4a950bbe19..e2e65aa99d6 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -208,7 +208,8 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O threadIds: threadInfo .filter { _, variant in variant == .contact } .map { id, _ in id }, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) // Remove the group requests @@ -218,7 +219,8 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O threadIds: threadInfo .filter { _, variant in variant == .legacyGroup || variant == .group } .map { id, _ in id }, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -257,7 +259,8 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O tableView: tableView, threadViewModel: threadViewModel, viewController: viewController, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: dependencies ) ) diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 86762ef3e42..5f1e75595d3 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -38,7 +38,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD verifyDBKeysAvailableBeforeBackgroundLaunch() _ = AppVersion.shared - AppEnvironment.shared.pushRegistrationManager.createVoipRegistryIfNecessary() + Singleton.setPushRegistrationManager(PushRegistrationManager(using: dependencies)) + Singleton.pushRegistrationManager.createVoipRegistryIfNecessary() // Prevent the device from sleeping during database view async registration // (e.g. long database upgrades). @@ -50,10 +51,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.loadingViewController = LoadingViewController() AppSetup.setupEnvironment( - appSpecificBlock: { + appSpecificBlock: { [dependencies] in Log.setup(with: Logger(primaryPrefix: "Session", level: .info)) Log.info("[AppDelegate] Setting up environment.") + /// Create a proper `SessionCallManager` for the main app (defaults to a no-op version) + Singleton.setCallManager(SessionCallManager(using: dependencies)) + // Setup LibSession LibSession.addLogger() LibSession.createNetworkIfNeeded() @@ -97,7 +101,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD using: dependencies ) - if SessionEnvironment.shared?.callManager.wrappedValue?.currentCall == nil { + if Singleton.callManager.currentCall == nil { UserDefaults.sharedLokiProject?[.isCallOngoing] = false UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil } @@ -687,7 +691,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Notifications func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - PushRegistrationManager.shared.didReceiveVanillaPushToken(deviceToken) + Singleton.pushRegistrationManager.didReceiveVanillaPushToken(deviceToken) Log.info("Registering for push notifications.") } @@ -696,9 +700,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD #if DEBUG Log.warn("We're in debug mode. Faking success for remote registration with a fake push identifier.") - PushRegistrationManager.shared.didReceiveVanillaPushToken(Data(count: 32)) + Singleton.pushRegistrationManager.didReceiveVanillaPushToken(Data(count: 32)) #else - PushRegistrationManager.shared.didFailToReceiveVanillaPushToken(error: error) + Singleton.pushRegistrationManager.didFailToReceiveVanillaPushToken(error: error) #endif } @@ -858,20 +862,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Call handling func hasIncomingCallWaiting() -> Bool { - guard let call = AppEnvironment.shared.callManager.currentCall else { return false } + guard let call = Singleton.callManager.currentCall else { return false } return !call.hasStartedConnecting } func hasCallOngoing() -> Bool { - guard let call = AppEnvironment.shared.callManager.currentCall else { return false } + guard let call = Singleton.callManager.currentCall else { return false } return !call.hasEnded } func handleAppActivatedWithOngoingCallIfNeeded() { guard - let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall), + let call: SessionCall = (Singleton.callManager.currentCall as? SessionCall), MiniCallView.current == nil, Singleton.hasAppContext else { return } diff --git a/Session/Meta/AppEnvironment.swift b/Session/Meta/AppEnvironment.swift index 6c9d0b27242..4ea25f00619 100644 --- a/Session/Meta/AppEnvironment.swift +++ b/Session/Meta/AppEnvironment.swift @@ -23,9 +23,7 @@ public class AppEnvironment { } } - public var callManager: SessionCallManager public var notificationPresenter: NotificationPresenter - public var pushRegistrationManager: PushRegistrationManager // Stored properties cannot be marked as `@available`, only classes and functions. // Instead, store a private `Any` and wrap it with a public `@available` getter @@ -36,9 +34,7 @@ public class AppEnvironment { } private init() { - self.callManager = SessionCallManager() self.notificationPresenter = NotificationPresenter() - self.pushRegistrationManager = PushRegistrationManager() self._userNotificationActionHandler = UserNotificationActionHandler() SwiftSingletons.register(self) @@ -46,9 +42,6 @@ public class AppEnvironment { public func setup() { // Hang certain singletons on Environment too. - SessionEnvironment.shared?.callManager.mutate { - $0 = callManager - } SessionEnvironment.shared?.notificationsManager.mutate { $0 = notificationPresenter } diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index d51f7c16445..1e60c809ec3 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -480,7 +480,7 @@ class NotificationActionHandler { // MARK: - - func markAsRead(userInfo: [AnyHashable: Any]) -> AnyPublisher { + func markAsRead(userInfo: [AnyHashable: Any], using dependencies: Dependencies) -> AnyPublisher { guard let threadId: String = userInfo[AppNotificationUserInfoKey.threadId] as? String else { return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil")) .eraseToAnyPublisher() @@ -491,7 +491,7 @@ class NotificationActionHandler { .eraseToAnyPublisher() } - return markAsRead(threadId: threadId) + return markAsRead(threadId: threadId, using: dependencies) } func reply( @@ -532,7 +532,8 @@ class NotificationActionHandler { db, threadId: threadId, threadVariant: thread.variant - ) + ), + using: dependencies ) return try MessageSender.preparedSendData( @@ -590,7 +591,7 @@ class NotificationActionHandler { .eraseToAnyPublisher() } - private func markAsRead(threadId: String) -> AnyPublisher { + private func markAsRead(threadId: String, using dependencies: Dependencies) -> AnyPublisher { return Storage.shared .writePublisher { db in guard @@ -617,7 +618,8 @@ class NotificationActionHandler { db, threadId: threadId, threadVariant: threadVariant - ) + ), + using: dependencies ) } .eraseToAnyPublisher() diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 04f91586572..971f9960790 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -8,17 +8,22 @@ import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -public enum PushRegistrationError: Error { - case assertionError(description: String) - case pushNotSupported(description: String) - case timeout - case publisherNoLongerExists +// MARK: - Singleton + +public extension Singleton { + // FIXME: This will be reworked to be part of dependencies in the Groups Rebuild branch + fileprivate static var _pushRegistrationManager: Atomic = Atomic(NoopPushRegistrationManager()) + static var pushRegistrationManager: PushRegistrationManagerType { _pushRegistrationManager.wrappedValue } + + static func setPushRegistrationManager(_ pushRegistrationManager: PushRegistrationManagerType) { + _pushRegistrationManager = Atomic(pushRegistrationManager) + } } -/** - * Singleton used to integrate with push notification services - registration and routing received remote notifications. - */ -@objc public class PushRegistrationManager: NSObject, PKPushRegistryDelegate { +// MARK: - PushRegistrationManager + +public class PushRegistrationManager: NSObject, PKPushRegistryDelegate, PushRegistrationManagerType { + private let dependencies: Dependencies // MARK: - Dependencies @@ -26,27 +31,20 @@ public enum PushRegistrationError: Error { return AppEnvironment.shared.notificationPresenter } - // MARK: - Singleton class - - @objc - public static var shared: PushRegistrationManager { - get { - return AppEnvironment.shared.pushRegistrationManager - } - } - - override init() { - super.init() - - SwiftSingletons.register(self) - } - private var vanillaTokenPublisher: AnyPublisher? private var vanillaTokenResolver: ((Result) -> ())? private var voipRegistry: PKPushRegistry? private var voipTokenPublisher: AnyPublisher? private var voipTokenResolver: ((Result) -> ())? + + // MARK: - Initialization + + public init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init() + } // MARK: - Public interface @@ -72,7 +70,7 @@ public enum PushRegistrationError: Error { // MARK: Vanilla push token // Vanilla push token is obtained from the system via AppDelegate - public func didReceiveVanillaPushToken(_ tokenData: Data, using dependencies: Dependencies = Dependencies()) { + public func didReceiveVanillaPushToken(_ tokenData: Data) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { Log.error("[PushRegistrationManager] Publisher completion in \(#function) unexpectedly nil") return @@ -83,8 +81,8 @@ public enum PushRegistrationError: Error { } } - // Vanilla push token is obtained from the system via AppDelegate - public func didFailToReceiveVanillaPushToken(error: Error, using dependencies: Dependencies = Dependencies()) { + // Vanilla push token is obtained from the system via AppDelegate + public func didFailToReceiveVanillaPushToken(error: Error) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { Log.error("[PushRegistrationManager] Publisher completion in \(#function) unexpectedly nil") return @@ -286,7 +284,7 @@ public enum PushRegistrationError: Error { let caller: String = payload["caller"] as? String, let timestampMs: Int64 = payload["timestamp"] as? Int64 else { - SessionCallManager.reportFakeCall(info: "Missing payload data") // stringlint:ignore + SessionCallManager.reportFakeCall(info: "Missing payload data", using: dependencies) // stringlint:ignore return } @@ -314,7 +312,7 @@ public enum PushRegistrationError: Error { } }() - let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) + let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer, using: dependencies) let thread: SessionThread = try SessionThread.upsert( db, id: caller, @@ -342,7 +340,7 @@ public enum PushRegistrationError: Error { } guard let call: SessionCall = maybeCall else { - SessionCallManager.reportFakeCall(info: "Could not retrieve call from database") // stringlint:ignore + SessionCallManager.reportFakeCall(info: "Could not retrieve call from database", using: dependencies) // stringlint:ignore return } @@ -363,3 +361,12 @@ fileprivate extension Data { return map { String(format: "%02hhx", $0) }.joined() // stringlint:ignore } } + +// MARK: - PushRegistrationError + +public enum PushRegistrationError: Error { + case assertionError(description: String) + case pushNotSupported(description: String) + case timeout + case publisherNoLongerExists +} diff --git a/Session/Notifications/PushRegistrationManagerType.swift b/Session/Notifications/PushRegistrationManagerType.swift index e69de29bb2d..8b2948e7b15 100644 --- a/Session/Notifications/PushRegistrationManagerType.swift +++ b/Session/Notifications/PushRegistrationManagerType.swift @@ -0,0 +1,29 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine + +// FIXME: Remove this in Groups Rebuild (redundant with the updated dependency management) +public protocol PushRegistrationManagerType { + func createVoipRegistryIfNecessary() + func didReceiveVanillaPushToken(_ tokenData: Data) + func didFailToReceiveVanillaPushToken(error: Error) + + func requestPushTokens() -> AnyPublisher<(pushToken: String, voipToken: String), Error> +} + +// MARK: - NoopPushRegistrationManager + +public class NoopPushRegistrationManager: PushRegistrationManagerType { + public func createVoipRegistryIfNecessary() {} + public func didReceiveVanillaPushToken(_ tokenData: Data) {} + public func didFailToReceiveVanillaPushToken(error: Error) {} + + public func requestPushTokens() -> AnyPublisher<(pushToken: String, voipToken: String), Error> { + return Fail( + error: PushRegistrationError.assertionError( + description: "Attempted to register with NoopPushRegistrationManager" // stringlint:ignore + ) + ).eraseToAnyPublisher() + } +} diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index fdcba678927..3c86c54d926 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -104,7 +104,7 @@ public enum SyncPushTokensJob: JobExecutor { /// **Note:** Apple's documentation states that we should re-register for notifications on every launch: /// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1 Log.info("[SyncPushTokensJob] Re-registering for remote notifications") - PushRegistrationManager.shared.requestPushTokens() + Singleton.pushRegistrationManager.requestPushTokens() .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<(String, String)?, Error> in Deferred { Future<(String, String)?, Error> { resolver in diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index b70c33acf10..4e0b38a7a9c 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -294,7 +294,7 @@ public class UserNotificationActionHandler: NSObject { switch action { case .markAsRead: - return actionHandler.markAsRead(userInfo: userInfo) + return actionHandler.markAsRead(userInfo: userInfo, using: dependencies) case .reply: guard let textInputResponse = response as? UNTextInputNotificationResponse else { diff --git a/Session/Onboarding/DisplayNameScreen.swift b/Session/Onboarding/DisplayNameScreen.swift index 1551f674339..efb7011909e 100644 --- a/Session/Onboarding/DisplayNameScreen.swift +++ b/Session/Onboarding/DisplayNameScreen.swift @@ -119,7 +119,7 @@ struct DisplayNameScreen: View { // If we are not in the registration flow then we are finished and should go straight // to the home screen guard self.flow == .register else { - self.flow.completeRegistration() + self.flow.completeRegistration(using: dependencies) let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index 9010591d822..9514eacc408 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -108,7 +108,7 @@ struct LoadingScreen: View { self.percentage = 1 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [dependencies] in - self.flow.completeRegistration() + self.flow.completeRegistration(using: dependencies) let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index dcf17541322..551a13d4b36 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -162,7 +162,8 @@ enum Onboarding { db, Contact.Columns.isTrusted.set(to: true), // Always trust the current user Contact.Columns.isApproved.set(to: true), - Contact.Columns.didApproveMe.set(to: true) + Contact.Columns.didApproveMe.set(to: true), + using: dependencies ) /// Create the 'Note to Self' thread (not visible by default) @@ -182,7 +183,8 @@ enum Onboarding { .filter(id: x25519PublicKey) .updateAllAndConfig( db, - SessionThread.Columns.shouldBeVisible.set(to: false) + SessionThread.Columns.shouldBeVisible.set(to: false), + using: dependencies ) } @@ -201,7 +203,7 @@ enum Onboarding { .sinkUntilComplete() } - func completeRegistration() { + func completeRegistration(using dependencies: Dependencies) { // Set the `lastNameUpdate` to the current date, so that we don't overwrite // what the user set in the display name step with whatever we find in their // swarm (otherwise the user could enter a display name and have it immediately @@ -211,7 +213,8 @@ enum Onboarding { .filter(id: getUserHexEncodedPublicKey(db)) .updateAllAndConfig( db, - Profile.Columns.lastNameUpdate.set(to: Date().timeIntervalSince1970) + Profile.Columns.lastNameUpdate.set(to: Date().timeIntervalSince1970), + using: dependencies ) } diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index 703499ef52a..ec9a8addec9 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -160,7 +160,7 @@ struct PNModeScreen: View { } private func finishRegister() { - self.flow.completeRegistration() + self.flow.completeRegistration(using: dependencies) let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index 764b55cc8fd..d2dbfef6613 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -205,12 +205,16 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo confirmTitle: "blockUnblock".localized(), confirmStyle: .danger, cancelStyle: .alert_text - ) { [weak self] _ in + ) { [weak self, dependencies] _ in // Unblock the contacts Storage.shared.write { db in _ = try Contact .filter(ids: contactIds) - .updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false)) + .updateAllAndConfig( + db, + Contact.Columns.isBlocked.set(to: false), + using: dependencies + ) } self?.selectedContactIdsSubject.send([]) diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 181622fb78c..ddb279b7c18 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -139,7 +139,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav } Storage.shared.write { db in - try db.setAndUpdateConfig(.isScreenLockEnabled, to: !db[.isScreenLockEnabled]) + try db.setAndUpdateConfig( + .isScreenLockEnabled, + to: !db[.isScreenLockEnabled], + using: dependencies + ) } } ) @@ -166,7 +170,8 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav Storage.shared.write { db in try db.setAndUpdateConfig( .checkForCommunityMessageRequests, - to: !db[.checkForCommunityMessageRequests] + to: !db[.checkForCommunityMessageRequests], + using: dependencies ) } } @@ -192,7 +197,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.areReadReceiptsEnabled, to: !db[.areReadReceiptsEnabled]) + try db.setAndUpdateConfig( + .areReadReceiptsEnabled, + to: !db[.areReadReceiptsEnabled], + using: dependencies + ) } } ) @@ -252,7 +261,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.typingIndicatorsEnabled, to: !db[.typingIndicatorsEnabled]) + try db.setAndUpdateConfig( + .typingIndicatorsEnabled, + to: !db[.typingIndicatorsEnabled], + using: dependencies + ) } } ) @@ -277,7 +290,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.areLinkPreviewsEnabled, to: !db[.areLinkPreviewsEnabled]) + try db.setAndUpdateConfig( + .areLinkPreviewsEnabled, + to: !db[.areLinkPreviewsEnabled], + using: dependencies + ) } } ) @@ -314,7 +331,11 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), onTap: { Storage.shared.write { db in - try db.setAndUpdateConfig(.areCallsEnabled, to: !db[.areCallsEnabled]) + try db.setAndUpdateConfig( + .areCallsEnabled, + to: !db[.areCallsEnabled], + using: dependencies + ) } } ) diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index c77e40d4167..ee8be3e41fd 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -50,7 +50,8 @@ public extension UIContextualAction { tableView: UITableView, threadViewModel: SessionThreadViewModel, viewController: UIViewController?, - navigatableStateHolder: NavigatableStateHolder? + navigatableStateHolder: NavigatableStateHolder?, + using dependencies: Dependencies ) -> [UIContextualAction]? { guard !actions.isEmpty else { return nil } @@ -106,10 +107,11 @@ public extension UIContextualAction { case true: threadViewModel.markAsRead( target: .threadAndInteractions( interactionsBeforeInclusive: threadViewModel.interactionId - ) + ), + using: dependencies ) - case false: threadViewModel.markAsUnread() + case false: threadViewModel.markAsUnread(using: dependencies) } } completionHandler(true) @@ -142,7 +144,8 @@ public extension UIContextualAction { db, type: .deleteContactConversationAndMarkHidden, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -188,7 +191,8 @@ public extension UIContextualAction { db, type: .hideContactConversation, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -235,7 +239,8 @@ public extension UIContextualAction { .updateAllAndConfig( db, SessionThread.Columns.pinnedPriority - .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)) + .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)), + using: dependencies ) } } @@ -337,7 +342,11 @@ public extension UIContextualAction { .save(db) try Contact .filter(id: threadViewModel.threadId) - .updateAllAndConfig(db, contactChanges) + .updateAllAndConfig( + db, + contactChanges, + using: dependencies + ) // Blocked message requests should be deleted if threadIsMessageRequest { @@ -345,7 +354,8 @@ public extension UIContextualAction { db, type: .deleteContactConversationAndMarkHidden, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -439,7 +449,8 @@ public extension UIContextualAction { db, type: deletionType, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } catch { DispatchQueue.main.async { @@ -551,7 +562,8 @@ public extension UIContextualAction { db, type: deletionType, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } diff --git a/SessionMessagingKit/Calls/CallManagerProtocol.swift b/SessionMessagingKit/Calls/CallManagerProtocol.swift index dcfc31aa4b5..b1b3587c50f 100644 --- a/SessionMessagingKit/Calls/CallManagerProtocol.swift +++ b/SessionMessagingKit/Calls/CallManagerProtocol.swift @@ -2,11 +2,33 @@ import Foundation import CallKit +import SessionUtilitiesKit + +// MARK: - Singleton + +public extension Singleton { + // FIXME: This will be reworked to be part of dependencies in the Groups Rebuild branch + fileprivate static var _callManager: Atomic = Atomic(NoopSessionCallManager()) + static var callManager: CallManagerProtocol { _callManager.wrappedValue } + + static func setCallManager(_ callManager: CallManagerProtocol) { + _callManager = Atomic(callManager) + } +} + +// MARK: - CallManagerProtocol public protocol CallManagerProtocol { var currentCall: CurrentCallProtocol? { get set } + func setCurrentCall(_ call: CurrentCallProtocol?) + func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) func reportCurrentCallEnded(reason: CXCallEndedReason?) + func suspendDatabaseIfCallEndedInBackground() + + func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) + func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) + func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) func handleICECandidates(message: CallMessage, sdpMLineIndexes: [UInt32], sdpMids: [String]) diff --git a/SessionMessagingKit/Calls/CurrentCallProtocol.swift b/SessionMessagingKit/Calls/CurrentCallProtocol.swift index c37a26bdd6e..bb6334c064f 100644 --- a/SessionMessagingKit/Calls/CurrentCallProtocol.swift +++ b/SessionMessagingKit/Calls/CurrentCallProtocol.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import WebRTC +import SessionUtilitiesKit public protocol CurrentCallProtocol { var uuid: String { get } @@ -10,7 +11,7 @@ public protocol CurrentCallProtocol { var hasStartedConnecting: Bool { get set } var hasEnded: Bool { get set } - func updateCallMessage(mode: EndCallMode) + func updateCallMessage(mode: EndCallMode, using dependencies: Dependencies) func didReceiveRemoteSDP(sdp: RTCSessionDescription) func startSessionCall(_ db: Database) } diff --git a/SessionMessagingKit/Calls/NoopSessionCallManager.swift b/SessionMessagingKit/Calls/NoopSessionCallManager.swift index e69de29bb2d..afa3e928060 100644 --- a/SessionMessagingKit/Calls/NoopSessionCallManager.swift +++ b/SessionMessagingKit/Calls/NoopSessionCallManager.swift @@ -0,0 +1,25 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import CallKit + +internal struct NoopSessionCallManager: CallManagerProtocol { + var currentCall: CurrentCallProtocol? + + func setCurrentCall(_ call: CurrentCallProtocol?) {} + func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) {} + func reportCurrentCallEnded(reason: CXCallEndedReason?) {} + func suspendDatabaseIfCallEndedInBackground() {} + + func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + + func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) {} + func handleICECandidates(message: CallMessage, sdpMLineIndexes: [UInt32], sdpMids: [String]) {} + func handleAnswerMessage(_ message: CallMessage) {} + + func currentWebRTCSessionMatches(callId: String) -> Bool { return false } + + func dismissAllCallUI() {} +} diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift index 4f26d29cebb..99d04b493d6 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift @@ -197,7 +197,7 @@ enum _014_GenerateInitialUserConfigDumps: Migration { // MARK: - Threads - try LibSession.updatingThreads(db, Array(allThreads.values)) + try LibSession.updatingThreads(db, Array(allThreads.values), using: dependencies) // MARK: - Syncing diff --git a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift b/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift index 36d2d45800e..23853421850 100644 --- a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift +++ b/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift @@ -80,7 +80,7 @@ enum _018_DisappearingMessagesConfiguration: Migration { } // Update the configs so the settings are synced - _ = try LibSession.updatingDisappearingConfigs(db, contactUpdate) + _ = try LibSession.updatingDisappearingConfigs(db, contactUpdate, using: dependencies) _ = try LibSession.batchUpdate(db, disappearingConfigs: legacyGroupUpdate, using: dependencies) Storage.update(progress: 1, for: self, in: target) // In case this is the last migration diff --git a/SessionMessagingKit/Database/Models/BlindedIdLookup.swift b/SessionMessagingKit/Database/Models/BlindedIdLookup.swift index 90a6a89c69c..a5bfce2201f 100644 --- a/SessionMessagingKit/Database/Models/BlindedIdLookup.swift +++ b/SessionMessagingKit/Database/Models/BlindedIdLookup.swift @@ -137,7 +137,11 @@ public extension BlindedIdLookup { if isCheckingForOutbox && !contact.isApproved { try Contact .filter(id: contact.id) - .updateAllAndConfig(db, Contact.Columns.isApproved.set(to: true)) + .updateAllAndConfig( + db, + Contact.Columns.isApproved.set(to: true), + using: dependencies + ) } break diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 046d612a02e..20fe72b8be8 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -115,13 +115,15 @@ public extension ClosedGroup { _ db: Database? = nil, threadId: String, removeGroupData: Bool, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { try removeKeysAndUnsubscribe( db, threadIds: [threadId], removeGroupData: removeGroupData, - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) } @@ -129,16 +131,18 @@ public extension ClosedGroup { _ db: Database? = nil, threadIds: [String], removeGroupData: Bool, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { guard !threadIds.isEmpty else { return } guard let db: Database = db else { - Storage.shared.write { db in + dependencies.storage.write { db in try ClosedGroup.removeKeysAndUnsubscribe( db, threadIds: threadIds, removeGroupData: removeGroupData, - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) } return @@ -196,7 +200,8 @@ public extension ClosedGroup { db, legacyGroupIds: threadVariants .filter { $0.variant == .legacyGroup } - .map { $0.id } + .map { $0.id }, + using: dependencies ) try LibSession.remove( diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index e87ce2b5c7b..bae6c46d94d 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -491,7 +491,8 @@ public extension Interaction { threadId: String, threadVariant: SessionThread.Variant, includingOlder: Bool, - trySendReadReceipt: Bool + trySendReadReceipt: Bool, + using dependencies: Dependencies ) throws { guard let interactionId: Int64 = interactionId else { return } @@ -535,7 +536,8 @@ public extension Interaction { ], lastReadTimestampMs: timestampMs, trySendReadReceipt: trySendReadReceipt, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) return } @@ -560,7 +562,8 @@ public extension Interaction { interactionInfo: [interactionInfo], lastReadTimestampMs: interactionInfo.timestampMs, trySendReadReceipt: trySendReadReceipt, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) return } @@ -576,7 +579,8 @@ public extension Interaction { interactionInfo: interactionInfoToMarkAsRead, lastReadTimestampMs: interactionInfo.timestampMs, trySendReadReceipt: trySendReadReceipt, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -647,7 +651,8 @@ public extension Interaction { interactionInfo: [Interaction.ReadInfo], lastReadTimestampMs: Int64, trySendReadReceipt: Bool, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { guard !interactionInfo.isEmpty else { return } @@ -657,7 +662,8 @@ public extension Interaction { db, threadId: threadId, threadVariant: threadVariant, - lastReadTimestampMs: lastReadTimestampMs + lastReadTimestampMs: lastReadTimestampMs, + using: dependencies ) // Add the 'DisappearingMessagesJob' if needed - this will update any expiring diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index b93bdd2329d..66aa787a5b5 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -304,8 +304,9 @@ public extension SessionThread { .filter(id: id) .updateAllAndConfig( db, + requiredChanges, calledFromConfig: calledFromConfig, - requiredChanges + using: dependencies ) /// We need to re-fetch the updated thread as the changes wouldn't have been applied to `result`, it's also possible additional @@ -423,13 +424,15 @@ public extension SessionThread { _ db: Database, type: SessionThread.DeletionType, threadId: String, - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { try deleteOrLeave( db, type: type, threadIds: [threadId], - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) } @@ -437,7 +440,8 @@ public extension SessionThread { _ db: Database, type: SessionThread.DeletionType, threadIds: [String], - calledFromConfigHandling: Bool + calledFromConfigHandling: Bool, + using dependencies: Dependencies ) throws { let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let remainingThreadIds: Set = threadIds.asSet().removing(currentUserPublicKey) @@ -450,9 +454,10 @@ public extension SessionThread { .filter(id: currentUserPublicKey) .updateAllAndConfig( db, - calledFromConfig: calledFromConfigHandling, SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false) + SessionThread.Columns.shouldBeVisible.set(to: false), + calledFromConfig: calledFromConfigHandling, + using: dependencies ) } @@ -461,9 +466,10 @@ public extension SessionThread { .filter(ids: remainingThreadIds) .updateAllAndConfig( db, - calledFromConfig: calledFromConfigHandling, SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false) + SessionThread.Columns.shouldBeVisible.set(to: false), + calledFromConfig: calledFromConfigHandling, + using: dependencies ) case .hideContactConversationAndDeleteContentDirectly: @@ -479,9 +485,10 @@ public extension SessionThread { .filter(id: currentUserPublicKey) .updateAllAndConfig( db, - calledFromConfig: calledFromConfigHandling, SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false) + SessionThread.Columns.shouldBeVisible.set(to: false), + calledFromConfig: calledFromConfigHandling, + using: dependencies ) } @@ -491,9 +498,10 @@ public extension SessionThread { .filter(ids: remainingThreadIds) .updateAllAndConfig( db, - calledFromConfig: calledFromConfigHandling, SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false) + SessionThread.Columns.shouldBeVisible.set(to: false), + calledFromConfig: calledFromConfigHandling, + using: dependencies ) case .deleteContactConversationAndMarkHidden: @@ -513,21 +521,22 @@ public extension SessionThread { .filter(id: currentUserPublicKey) .updateAllAndConfig( db, - calledFromConfig: calledFromConfigHandling, SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false) + SessionThread.Columns.shouldBeVisible.set(to: false), + calledFromConfig: calledFromConfigHandling, + using: dependencies ) } if !calledFromConfigHandling { // Update any other threads to be hidden - try LibSession.hide(db, contactIds: Array(remainingThreadIds)) + try LibSession.hide(db, contactIds: Array(remainingThreadIds), using: dependencies) } case .deleteContactConversationAndContact: // If this wasn't called from config handling then we need to hide the conversation if !calledFromConfigHandling { - try LibSession.remove(db, contactIds: Array(remainingThreadIds)) + try LibSession.remove(db, contactIds: Array(remainingThreadIds), using: dependencies) } _ = try SessionThread @@ -548,7 +557,8 @@ public extension SessionThread { db, threadIds: threadIds, removeGroupData: true, - calledFromConfigHandling: calledFromConfigHandling + calledFromConfigHandling: calledFromConfigHandling, + using: dependencies ) case .deleteCommunityAndContent: diff --git a/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift index c771eb5ff4a..e3976ea959f 100644 --- a/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift @@ -127,7 +127,8 @@ public enum GroupLeavingJob: JobExecutor { db, threadId: threadId, removeGroupData: details.deleteThread, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index ba5879a7779..9175c04a0fe 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -151,7 +151,8 @@ internal extension LibSession { db, type: .deleteContactConversationAndMarkHidden, threadId: sessionId, - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) /// We need to create or update the thread @@ -250,10 +251,15 @@ internal extension LibSession { db, type: .deleteContactConversationAndContact, threadIds: combinedIds, - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) - try LibSession.remove(db, volatileContactIds: combinedIds) + try LibSession.remove( + db, + volatileContactIds: combinedIds, + using: dependencies + ) } } @@ -365,7 +371,11 @@ internal extension LibSession { // MARK: - Outgoing Changes internal extension LibSession { - static func updatingContacts(_ db: Database, _ updated: [T]) throws -> [T] { + static func updatingContacts( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedContacts: [Contact] = updated as? [Contact] else { throw StorageError.generic } // The current users contact data doesn't need to sync so exclude it, we also don't want to sync @@ -383,7 +393,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in // When inserting new contacts (or contacts with invalid profile data) we want // to add any valid profile information we have so identify if any of the updated @@ -425,7 +436,11 @@ internal extension LibSession { return updated } - static func updatingProfiles(_ db: Database, _ updated: [T]) throws -> [T] { + static func updatingProfiles( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedProfiles: [Profile] = updated as? [Profile] else { throw StorageError.generic } // We should only sync profiles which are associated to contact data to avoid including profiles @@ -456,7 +471,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.update( profile: updatedUserProfile, @@ -468,7 +484,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession .upsert( @@ -481,7 +498,11 @@ internal extension LibSession { return updated } - static func updatingDisappearingConfigs(_ db: Database, _ updated: [T]) throws -> [T] { + static func updatingDisappearingConfigs( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedDisappearingConfigs: [DisappearingMessagesConfiguration] = updated as? [DisappearingMessagesConfiguration] else { throw StorageError.generic } // We should only sync disappearing messages configs which are associated to existing contacts @@ -510,7 +531,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateNoteToSelf( disappearingMessagesConfig: updatedUserDisappearingConfig, @@ -522,7 +544,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession .upsert( @@ -539,11 +562,16 @@ internal extension LibSession { // MARK: - External Outgoing Changes public extension LibSession { - static func hide(_ db: Database, contactIds: [String]) throws { + static func hide( + _ db: Database, + contactIds: [String], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in // Mark the contacts as hidden try LibSession.upsert( @@ -559,13 +587,18 @@ public extension LibSession { } } - static func remove(_ db: Database, contactIds: [String]) throws { + static func remove( + _ db: Database, + contactIds: [String], + using dependencies: Dependencies + ) throws { guard !contactIds.isEmpty else { return } try LibSession.performAndPushChange( db, for: .contacts, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in contactIds.forEach { sessionId in guard var cSessionId: [CChar] = sessionId.cString(using: .utf8) else { return } @@ -579,7 +612,8 @@ public extension LibSession { static func update( _ db: Database, sessionId: String, - disappearingMessagesConfig: DisappearingMessagesConfiguration + disappearingMessagesConfig: DisappearingMessagesConfiguration, + using dependencies: Dependencies ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -588,7 +622,8 @@ public extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateNoteToSelf( disappearingMessagesConfig: disappearingMessagesConfig, @@ -600,7 +635,8 @@ public extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession .upsert( diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift index 65ec663bcfa..aecd92557f8 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift @@ -17,7 +17,8 @@ internal extension LibSession { static func handleConvoInfoVolatileUpdate( _ db: Database, in conf: UnsafeMutablePointer?, - mergeNeedsDump: Bool + mergeNeedsDump: Bool, + using dependencies: Dependencies ) throws { guard mergeNeedsDump else { return } guard conf != nil else { throw LibSessionError.nilConfigObject } @@ -97,7 +98,8 @@ internal extension LibSession { interactionInfo: interactionInfoToMarkAsRead, lastReadTimestampMs: lastReadTimestampMs, trySendReadReceipt: false, // Interactions already read, no need to send - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) return nil } @@ -226,7 +228,8 @@ internal extension LibSession { static func updateMarkedAsUnreadState( _ db: Database, - threads: [SessionThread] + threads: [SessionThread], + using dependencies: Dependencies ) throws { // If we have no updated threads then no need to continue guard !threads.isEmpty else { return } @@ -245,7 +248,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db), + using: dependencies ) { conf in try upsert( convoInfoVolatileChanges: changes, @@ -254,11 +258,16 @@ internal extension LibSession { } } - static func remove(_ db: Database, volatileContactIds: [String]) throws { + static func remove( + _ db: Database, + volatileContactIds: [String], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try volatileContactIds.forEach { contactId in var cSessionId: [CChar] = try contactId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -269,11 +278,16 @@ internal extension LibSession { } } - static func remove(_ db: Database, volatileLegacyGroupIds: [String]) throws { + static func remove( + _ db: Database, + volatileLegacyGroupIds: [String], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try volatileLegacyGroupIds.forEach { legacyGroupId in var cLegacyGroupId: [CChar] = try legacyGroupId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -284,11 +298,16 @@ internal extension LibSession { } } - static func remove(_ db: Database, volatileCommunityInfo: [OpenGroupUrlInfo]) throws { + static func remove( + _ db: Database, + volatileCommunityInfo: [OpenGroupUrlInfo], + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try volatileCommunityInfo.forEach { urlInfo in var cBaseUrl: [CChar] = try urlInfo.server.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -308,12 +327,14 @@ public extension LibSession { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - lastReadTimestampMs: Int64 + lastReadTimestampMs: Int64, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .convoInfoVolatile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try upsert( convoInfoVolatileChanges: [ diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 80f858686bf..9409148805e 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -14,8 +14,9 @@ public extension LibSession { public typealias Domain = String } - /// The default priority for newly created threads - static var defaultNewThreadPriority: Int32 { return visiblePriority } + /// The default priority for newly created threads - the default value is for threads to be hidden as we explicitly make threads visible + /// when sending or receiving messages + static var defaultNewThreadPriority: Int32 { return hiddenPriority } /// A `0` `priority` value indicates visible, but not pinned static let visiblePriority: Int32 = 0 @@ -57,7 +58,7 @@ internal extension LibSession { _ db: Database, for variant: ConfigDump.Variant, publicKey: String, - using dependencies: Dependencies = Dependencies(), + using dependencies: Dependencies, change: (UnsafeMutablePointer?) throws -> () ) throws { // Since we are doing direct memory manipulation we are using an `Atomic` @@ -99,7 +100,11 @@ internal extension LibSession { } } - @discardableResult static func updatingThreads(_ db: Database, _ updated: [T]) throws -> [T] { + @discardableResult static func updatingThreads( + _ db: Database, + _ updated: [T], + using dependencies: Dependencies + ) throws -> [T] { guard let updatedThreads: [SessionThread] = updated as? [SessionThread] else { throw StorageError.generic } @@ -115,7 +120,7 @@ internal extension LibSession { .reduce(into: [:]) { result, next in result[next.threadId] = next } // Update the unread state for the threads first (just in case that's what changed) - try LibSession.updateMarkedAsUnreadState(db, threads: updatedThreads) + try LibSession.updateMarkedAsUnreadState(db, threads: updatedThreads, using: dependencies) // Then update the `hidden` and `priority` values try groupedThreads.forEach { variant, threads in @@ -127,7 +132,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateNoteToSelf( priority: { @@ -150,7 +156,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .contacts, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.upsert( contactData: remainingThreads @@ -174,7 +181,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.upsert( communities: threads @@ -196,7 +204,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.upsert( legacyGroups: threads @@ -240,7 +249,11 @@ internal extension LibSession { } } - static func updatingSetting(_ db: Database, _ updated: Setting?) throws { + static func updatingSetting( + _ db: Database, + _ updated: Setting?, + using dependencies: Dependencies + ) throws { // Don't current support any nullable settings guard let updatedSetting: Setting = updated else { return } @@ -252,7 +265,8 @@ internal extension LibSession { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: userPublicKey + publicKey: userPublicKey, + using: dependencies ) { conf in try LibSession.updateSettings( checkForCommunityMessageRequests: updatedSetting.unsafeValue(as: Bool.self), diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index d489f22de98..08797ea0959 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -181,7 +181,8 @@ internal extension LibSession { db, type: .deleteCommunityAndContent, threadIds: Array(communityIdsToRemove), - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) } @@ -365,13 +366,13 @@ internal extension LibSession { if !legacyGroupIdsToRemove.isEmpty { LibSession.kickFromConversationUIIfNeeded(removedThreadIds: Array(legacyGroupIdsToRemove)) - try SessionThread - .deleteOrLeave( - db, - type: .deleteGroupAndContent, - threadIds: Array(legacyGroupIdsToRemove), - calledFromConfigHandling: true - ) + try SessionThread.deleteOrLeave( + db, + type: .deleteGroupAndContent, + threadIds: Array(legacyGroupIdsToRemove), + calledFromConfigHandling: true, + using: dependencies + ) } // MARK: -- Handle Group Changes @@ -546,12 +547,14 @@ public extension LibSession { _ db: Database, server: String, rootToken: String, - publicKey: String + publicKey: String, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try LibSession.upsert( communities: [ @@ -569,11 +572,17 @@ public extension LibSession { } } - static func remove(_ db: Database, server: String, roomToken: String) throws { + static func remove( + _ db: Database, + server: String, + roomToken: String, + using dependencies: Dependencies + ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in var cBaseUrl: [CChar] = try server.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() var cRoom: [CChar] = try roomToken.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() @@ -592,7 +601,8 @@ public extension LibSession { roomToken: roomToken, publicKey: "" ) - ] + ], + using: dependencies ) } @@ -608,12 +618,14 @@ public extension LibSession { latestKeyPairReceivedTimestamp: TimeInterval, disappearingConfig: DisappearingMessagesConfiguration, members: Set, - admins: Set + admins: Set, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in guard conf != nil else { throw LibSessionError.nilConfigObject } @@ -674,12 +686,14 @@ public extension LibSession { latestKeyPair: ClosedGroupKeyPair? = nil, disappearingConfig: DisappearingMessagesConfiguration? = nil, members: Set? = nil, - admins: Set? = nil + admins: Set? = nil, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in try LibSession.upsert( legacyGroups: [ @@ -736,13 +750,18 @@ public extension LibSession { } } - static func remove(_ db: Database, legacyGroupIds: [String]) throws { + static func remove( + _ db: Database, + legacyGroupIds: [String], + using dependencies: Dependencies + ) throws { guard !legacyGroupIds.isEmpty else { return } try LibSession.performAndPushChange( db, for: .userGroups, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies ) { conf in legacyGroupIds.forEach { threadId in guard var cGroupId: [CChar] = threadId.cString(using: .utf8) else { return } @@ -753,7 +772,7 @@ public extension LibSession { } // Remove the volatile info as well - try LibSession.remove(db, volatileLegacyGroupIds: legacyGroupIds) + try LibSession.remove(db, volatileLegacyGroupIds: legacyGroupIds, using: dependencies) } // MARK: -- Group Changes diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 049e697ad10..3b152d07120 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -93,7 +93,8 @@ internal extension LibSession { db, type: .hideContactConversation, threadId: userPublicKey, - calledFromConfigHandling: true + calledFromConfigHandling: true, + using: dependencies ) } else { @@ -216,12 +217,14 @@ public extension LibSession { static func updateNoteToSelf( _ db: Database, priority: Int32? = nil, - disappearingMessagesConfig: DisappearingMessagesConfiguration? = nil + disappearingMessagesConfig: DisappearingMessagesConfiguration? = nil, + using dependencies: Dependencies ) throws { try LibSession.performAndPushChange( db, for: .userProfile, - publicKey: getUserHexEncodedPublicKey(db) + publicKey: getUserHexEncodedPublicKey(db), + using: dependencies ) { conf in try LibSession.updateNoteToSelf( priority: priority, diff --git a/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift b/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift index 303a1c65b18..772bd3f2a55 100644 --- a/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift +++ b/SessionMessagingKit/LibSession/Database/QueryInterfaceRequest+Utilities.swift @@ -52,17 +52,24 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table @discardableResult func updateAllAndConfig( _ db: Database, + _ assignments: ConfigColumnAssignment..., calledFromConfig: Bool = false, - _ assignments: ConfigColumnAssignment... + using dependencies: Dependencies ) throws -> Int { - return try updateAllAndConfig(db, calledFromConfig: calledFromConfig, assignments) + return try updateAllAndConfig( + db, + assignments, + calledFromConfig: calledFromConfig, + using: dependencies + ) } @discardableResult func updateAllAndConfig( _ db: Database, + _ assignments: [ConfigColumnAssignment], calledFromConfig: Bool = false, - _ assignments: [ConfigColumnAssignment] + using dependencies: Dependencies ) throws -> Int { let targetAssignments: [ColumnAssignment] = assignments.map { $0.assignment } @@ -71,7 +78,12 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table return try self.updateAll(db, targetAssignments) } - return try self.updateAndFetchAllAndUpdateConfig(db, calledFromConfig: calledFromConfig, assignments).count + return try self.updateAndFetchAllAndUpdateConfig( + db, + assignments, + calledFromConfig: calledFromConfig, + using: dependencies + ).count } // MARK: -- updateAndFetchAll @@ -79,17 +91,24 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table @discardableResult func updateAndFetchAllAndUpdateConfig( _ db: Database, + _ assignments: ConfigColumnAssignment..., calledFromConfig: Bool = false, - _ assignments: ConfigColumnAssignment... + using dependencies: Dependencies ) throws -> [RowDecoder] { - return try updateAndFetchAllAndUpdateConfig(db, calledFromConfig: calledFromConfig, assignments) + return try updateAndFetchAllAndUpdateConfig( + db, + assignments, + calledFromConfig: calledFromConfig, + using: dependencies + ) } @discardableResult func updateAndFetchAllAndUpdateConfig( _ db: Database, + _ assignments: [ConfigColumnAssignment], calledFromConfig: Bool = false, - _ assignments: [ConfigColumnAssignment] + using dependencies: Dependencies ) throws -> [RowDecoder] { // First perform the actual updates let updatedData: [RowDecoder] = try self.updateAndFetchAll(db, assignments.map { $0.assignment }) @@ -114,13 +133,13 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table // Update the config dump state where needed switch self { case is QueryInterfaceRequest: - return try LibSession.updatingContacts(db, updatedData) + return try LibSession.updatingContacts(db, updatedData, using: dependencies) case is QueryInterfaceRequest: - return try LibSession.updatingProfiles(db, updatedData) + return try LibSession.updatingProfiles(db, updatedData, using: dependencies) case is QueryInterfaceRequest: - return try LibSession.updatingThreads(db, updatedData) + return try LibSession.updatingThreads(db, updatedData, using: dependencies) default: return updatedData } diff --git a/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift b/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift index eeaf760cf9d..efcb9f2feff 100644 --- a/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift +++ b/SessionMessagingKit/LibSession/Database/Setting+Utilities.swift @@ -5,39 +5,103 @@ import GRDB import SessionUtilitiesKit public extension Database { - func setAndUpdateConfig(_ key: Setting.BoolKey, to newValue: Bool) throws { - try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + func setAndUpdateConfig( + _ key: Setting.BoolKey, + to newValue: Bool, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - func setAndUpdateConfig(_ key: Setting.DoubleKey, to newValue: Double?) throws { - try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + func setAndUpdateConfig( + _ key: Setting.DoubleKey, + to newValue: Double?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - func setAndUpdateConfig(_ key: Setting.IntKey, to newValue: Int?) throws { - try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + func setAndUpdateConfig( + _ key: Setting.IntKey, + to newValue: Int?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - func setAndUpdateConfig(_ key: Setting.StringKey, to newValue: String?) throws { - try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + func setAndUpdateConfig( + _ key: Setting.StringKey, + to newValue: String?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - func setAndUpdateConfig(_ key: Setting.EnumKey, to newValue: T?) throws { - try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + func setAndUpdateConfig( + _ key: Setting.EnumKey, + to newValue: T?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } - func setAndUpdateConfig(_ key: Setting.EnumKey, to newValue: T?) throws { - try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + func setAndUpdateConfig( + _ key: Setting.EnumKey, + to newValue: T?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } /// Value will be stored as a timestamp in seconds since 1970 - func setAndUpdateConfig(_ key: Setting.DateKey, to newValue: Date?) throws { - try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + func setAndUpdateConfig( + _ key: Setting.DateKey, + to newValue: Date?, + using dependencies: Dependencies + ) throws { + try updateConfigIfNeeded( + self, + key: key.rawValue, + updatedSetting: self.setting(key: key, to: newValue), + using: dependencies + ) } private func updateConfigIfNeeded( _ db: Database, key: String, - updatedSetting: Setting? + updatedSetting: Setting?, + using dependencies: Dependencies ) throws { // Before we do anything custom make sure the setting should trigger a change guard LibSession.syncedSettings.contains(key) else { return } @@ -53,6 +117,6 @@ public extension Database { } } - try LibSession.updatingSetting(db, updatedSetting) + try LibSession.updatingSetting(db, updatedSetting, using: dependencies) } } diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index b1bb6e54b31..7fe2c731ca4 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -79,7 +79,7 @@ public extension LibSession { // If we weren't given a database instance then get one guard let db: Database = db else { - Storage.shared.read { db in + dependencies.storage.read { db in LibSession.loadState(db, userPublicKey: userPublicKey, ed25519SecretKey: secretKey, using: dependencies) } return @@ -465,7 +465,8 @@ public extension LibSession { try LibSession.handleConvoInfoVolatileUpdate( db, in: conf, - mergeNeedsDump: config_needs_dump(conf) + mergeNeedsDump: config_needs_dump(conf), + using: dependencies ) case .userGroups: diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index e6a61249bc0..d10cdd34e2c 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -240,7 +240,8 @@ public final class OpenGroupManager { .updateAllAndConfig( db, OpenGroup.Columns.isActive.set(to: true), - OpenGroup.Columns.sequenceNumber.set(to: 0) + OpenGroup.Columns.sequenceNumber.set(to: 0), + using: dependencies ) } @@ -290,7 +291,8 @@ public final class OpenGroupManager { db, server: server, rootToken: roomToken, - publicKey: publicKey + publicKey: publicKey, + using: dependencies ) } @@ -395,11 +397,15 @@ public final class OpenGroupManager { // If it's a session-run room then just set it to inactive _ = try? OpenGroup .filter(id: openGroupId) - .updateAllAndConfig(db, OpenGroup.Columns.isActive.set(to: false)) + .updateAllAndConfig( + db, + OpenGroup.Columns.isActive.set(to: false), + using: dependencies + ) } if !calledFromConfigHandling, let server: String = server, let roomToken: String = roomToken { - try? LibSession.remove(db, server: server, roomToken: roomToken) + try? LibSession.remove(db, server: server, roomToken: roomToken, using: dependencies) } } @@ -478,7 +484,7 @@ public final class OpenGroupManager { try OpenGroup .filter(id: openGroup.id) - .updateAllAndConfig(db, changes) + .updateAllAndConfig(db, changes, using: dependencies) // Update the admin/moderator group members if let roomDetails: OpenGroupAPI.Room = pollInfo.details { @@ -764,7 +770,7 @@ public final class OpenGroupManager { switch processedMessage { case .config, .none: break - case .standard(let threadId, _, let proto, let messageInfo): + case .standard(_, _, let proto, let messageInfo): // We want to update the BlindedIdLookup cache with the message info so we can avoid using the // "expensive" lookup when possible let lookup: BlindedIdLookup = try { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 8784ddb4b53..55d8686315c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -25,7 +25,7 @@ extension MessageReceiver { case .provisionalAnswer: break // TODO: Implement case let .iceCandidates(sdpMLineIndexes, sdpMids): - SessionEnvironment.shared?.callManager.wrappedValue?.handleICECandidates( + Singleton.callManager.handleICECandidates( message: message, sdpMLineIndexes: sdpMLineIndexes, sdpMids: sdpMids @@ -116,15 +116,12 @@ extension MessageReceiver { return } - // Ensure we have a call manager before continuing - guard let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue else { return } - // Ignore pre offer message after the same call instance has been generated - if let currentCall: CurrentCallProtocol = callManager.currentCall, currentCall.uuid == message.uuid { + if let currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid { return } - guard callManager.currentCall == nil else { + guard Singleton.callManager.currentCall == nil else { try MessageReceiver.handleIncomingCallOfferInBusyState(db, message: message) return } @@ -132,7 +129,7 @@ extension MessageReceiver { let interaction: Interaction? = try MessageReceiver.insertCallInfoMessage(db, for: message, using: dependencies) // Handle UI - callManager.showCallUIForCall( + Singleton.callManager.showCallUIForCall( caller: sender, uuid: message.uuid, mode: .answer, @@ -145,8 +142,7 @@ extension MessageReceiver { // Ensure we have a call manager before continuing guard - let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue, - let currentCall: CurrentCallProtocol = callManager.currentCall, + let currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid, let sdp: String = message.sdps.first else { return } @@ -159,9 +155,8 @@ extension MessageReceiver { SNLog("[Calls] Received answer message.") guard - let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue, - callManager.currentWebRTCSessionMatches(callId: message.uuid), - var currentCall: CurrentCallProtocol = callManager.currentCall, + Singleton.callManager.currentWebRTCSessionMatches(callId: message.uuid), + var currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid, let sender: String = message.sender else { return } @@ -169,8 +164,8 @@ extension MessageReceiver { guard sender != getUserHexEncodedPublicKey(db) else { guard !currentCall.hasStartedConnecting else { return } - callManager.dismissAllCallUI() - callManager.reportCurrentCallEnded(reason: .answeredElsewhere) + Singleton.callManager.dismissAllCallUI() + Singleton.callManager.reportCurrentCallEnded(reason: .answeredElsewhere) return } guard let sdp: String = message.sdps.first else { return } @@ -178,22 +173,21 @@ extension MessageReceiver { let sdpDescription: RTCSessionDescription = RTCSessionDescription(type: .answer, sdp: sdp) currentCall.hasStartedConnecting = true currentCall.didReceiveRemoteSDP(sdp: sdpDescription) - callManager.handleAnswerMessage(message) + Singleton.callManager.handleAnswerMessage(message) } private static func handleEndCallMessage(_ db: Database, message: CallMessage) { SNLog("[Calls] Received end call message.") guard - let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue, - callManager.currentWebRTCSessionMatches(callId: message.uuid), - let currentCall: CurrentCallProtocol = callManager.currentCall, + Singleton.callManager.currentWebRTCSessionMatches(callId: message.uuid), + let currentCall: CurrentCallProtocol = Singleton.callManager.currentCall, currentCall.uuid == message.uuid, let sender: String = message.sender else { return } - callManager.dismissAllCallUI() - callManager.reportCurrentCallEnded( + Singleton.callManager.dismissAllCallUI() + Singleton.callManager.reportCurrentCallEnded( reason: (sender == getUserHexEncodedPublicKey(db) ? .declinedElsewhere : .remoteEnded diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 68515f84d37..be26329df93 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -31,7 +31,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .membersAdded: @@ -39,7 +40,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .membersRemoved: @@ -47,7 +49,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .memberLeft: @@ -55,7 +58,8 @@ extension MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case .encryptionKeyPairRequest: break // Currently not used @@ -234,7 +238,8 @@ extension MessageReceiver { latestKeyPairReceivedTimestamp: receivedTimestamp, disappearingConfig: disappearingConfig, members: members.asSet(), - admins: admins.asSet() + admins: admins.asSet(), + using: dependencies ) } @@ -336,7 +341,8 @@ extension MessageReceiver { try? LibSession.update( db, groupPublicKey: groupPublicKey, - latestKeyPair: keyPair + latestKeyPair: keyPair, + using: dependencies ) } catch { @@ -354,7 +360,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -373,7 +380,8 @@ extension MessageReceiver { try? LibSession.update( db, groupPublicKey: threadId, - name: name + name: name, + using: dependencies ) _ = try ClosedGroup @@ -390,7 +398,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -424,7 +433,8 @@ extension MessageReceiver { admins: allMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Create records for any new members @@ -479,7 +489,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -531,7 +542,8 @@ extension MessageReceiver { admins: allMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Delete the removed members @@ -552,7 +564,8 @@ extension MessageReceiver { db, threadId: threadId, removeGroupData: true, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } } @@ -567,7 +580,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: ClosedGroupControlMessage + message: ClosedGroupControlMessage, + using dependencies: Dependencies ) throws { guard let messageKind: ClosedGroupControlMessage.Kind = message.kind, @@ -606,7 +620,8 @@ extension MessageReceiver { admins: allMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Delete the members to remove @@ -620,7 +635,8 @@ extension MessageReceiver { db, threadId: threadId, removeGroupData: (sender == userPublicKey), - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift index 44f2c0f5b9e..74ff4272e5a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift @@ -12,7 +12,8 @@ extension MessageReceiver { threadVariant: SessionThread.Variant, message: ExpirationTimerUpdate, serverExpirationTimestamp: TimeInterval?, - proto: SNProtoContent + proto: SNProtoContent, + using dependencies: Dependencies ) throws { guard proto.hasExpirationType || proto.hasExpirationTimer else { return } guard @@ -52,7 +53,8 @@ extension MessageReceiver { .update( db, groupPublicKey: threadId, - disappearingConfig: updatedConfig + disappearingConfig: updatedConfig, + using: dependencies ) } fallthrough @@ -81,7 +83,8 @@ extension MessageReceiver { _ db: Database, messageVariant: Message.Variant?, contactId: String?, - version: FeatureVersion? + version: FeatureVersion?, + using dependencies: Dependencies ) { guard let messageVariant: Message.Variant = messageVariant, @@ -97,7 +100,8 @@ extension MessageReceiver { .filter(id: contactId) .updateAllAndConfig( db, - Contact.Columns.lastKnownClientVersion.set(to: version) + Contact.Columns.lastKnownClientVersion.set(to: version), + using: dependencies ) } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index ca0cbd40f14..e83796f4a49 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -118,7 +118,8 @@ extension MessageReceiver { db, type: .deleteContactConversationAndContact, // Blinded contact isn't synced anyway threadId: blindedIdLookup.blindedId, - calledFromConfigHandling: false + calledFromConfigHandling: false, + using: dependencies ) } @@ -126,7 +127,8 @@ extension MessageReceiver { try updateContactApprovalStatusIfNeeded( db, senderSessionId: senderId, - threadId: nil + threadId: nil, + using: dependencies ) // If there were blinded contacts which have now been resolved to this contact then we should remove @@ -140,7 +142,8 @@ extension MessageReceiver { try updateContactApprovalStatusIfNeeded( db, senderSessionId: userPublicKey, - threadId: unblindedThread.id + threadId: unblindedThread.id, + using: dependencies ) } @@ -165,7 +168,8 @@ extension MessageReceiver { internal static func updateContactApprovalStatusIfNeeded( _ db: Database, senderSessionId: String, - threadId: String? + threadId: String?, + using dependencies: Dependencies ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -188,7 +192,11 @@ extension MessageReceiver { try? contact.save(db) _ = try? Contact .filter(id: threadId) - .updateAllAndConfig(db, Contact.Columns.isApproved.set(to: true)) + .updateAllAndConfig( + db, + Contact.Columns.isApproved.set(to: true), + using: dependencies + ) } else { // The message was sent to the current user so flag their 'didApproveMe' as true (can't send a message to @@ -200,7 +208,11 @@ extension MessageReceiver { try? contact.save(db) _ = try? Contact .filter(id: senderSessionId) - .updateAllAndConfig(db, Contact.Columns.didApproveMe.set(to: true)) + .updateAllAndConfig( + db, + Contact.Columns.didApproveMe.set(to: true), + using: dependencies + ) } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index 8f7d7065c93..b4648c54c44 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -10,7 +10,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: UnsendRequest + message: UnsendRequest, + using dependencies: Dependencies ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -35,7 +36,8 @@ extension MessageReceiver { threadId: interaction.threadId, threadVariant: threadVariant, includingOlder: false, - trySendReadReceipt: false + trySendReadReceipt: false, + using: dependencies ) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: interaction.notificationIdentifiers) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 126d39e91dd..ef9a21b7aae 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -13,7 +13,7 @@ extension MessageReceiver { message: VisibleMessage, serverExpirationTimestamp: TimeInterval?, associatedWithProto proto: SNProtoContent, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) throws -> Int64 { guard let sender: String = message.sender, let dataMessage = proto.dataMessage else { throw MessageReceiverError.invalidMessage @@ -212,7 +212,8 @@ extension MessageReceiver { interactionId: existingInteractionId, messageSentTimestamp: messageSentTimestamp, variant: variant, - syncTarget: message.syncTarget + syncTarget: message.syncTarget, + using: dependencies ) Message.getExpirationForOutgoingDisappearingMessages( @@ -239,7 +240,8 @@ extension MessageReceiver { interactionId: interactionId, messageSentTimestamp: messageSentTimestamp, variant: variant, - syncTarget: message.syncTarget + syncTarget: message.syncTarget, + using: dependencies ) if messageExpirationInfo.shouldUpdateExpiry { @@ -356,7 +358,8 @@ extension MessageReceiver { try MessageReceiver.updateContactApprovalStatusIfNeeded( db, senderSessionId: sender, - threadId: thread.id + threadId: thread.id, + using: dependencies ) } @@ -463,7 +466,8 @@ extension MessageReceiver { interactionId: Int64, messageSentTimestamp: TimeInterval, variant: Interaction.Variant, - syncTarget: String? + syncTarget: String?, + using dependencies: Dependencies ) throws { guard variant == .standardOutgoing else { return } @@ -487,7 +491,8 @@ extension MessageReceiver { threadId: thread.id, threadVariant: thread.variant, includingOlder: true, - trySendReadReceipt: false + trySendReadReceipt: false, + using: dependencies ) // Process any PendingReadReceipt values diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index 0da981c1a2b..05d7b56a14d 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -93,7 +93,8 @@ extension MessageSender { latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp, disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey), members: members, - admins: admins + admins: admins, + using: dependencies ) let memberSendData: [MessageSender.PreparedSendData] = try members @@ -274,7 +275,8 @@ extension MessageSender { admins: allGroupMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) } @@ -344,7 +346,8 @@ extension MessageSender { try? LibSession.update( db, groupPublicKey: closedGroup.threadId, - name: name + name: name, + using: dependencies ) } @@ -457,7 +460,8 @@ extension MessageSender { admins: allGroupMembers .filter { $0.role == .admin } .map { $0.profileId } - .asSet() + .asSet(), + using: dependencies ) // Send the update to the group diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 6b699fc8fd1..9caeaaa5bbc 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -282,7 +282,8 @@ public enum MessageReceiver { version: ((!proto.hasExpirationType && !proto.hasExpirationTimer) ? .legacyDisappearingMessages : .newDisappearingMessages - ) + ), + using: dependencies ) switch message { @@ -327,7 +328,8 @@ public enum MessageReceiver { threadVariant: threadVariant, message: message, serverExpirationTimestamp: serverExpirationTimestamp, - proto: proto + proto: proto, + using: dependencies ) case let message as UnsendRequest: @@ -335,7 +337,8 @@ public enum MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case let message as CallMessage: @@ -361,21 +364,29 @@ public enum MessageReceiver { threadVariant: threadVariant, message: message, serverExpirationTimestamp: serverExpirationTimestamp, - associatedWithProto: proto + associatedWithProto: proto, + using: dependencies ) default: throw MessageReceiverError.unknownMessage } // Perform any required post-handling logic - try MessageReceiver.postHandleMessage(db, threadId: threadId, threadVariant: threadVariant, message: message) + try MessageReceiver.postHandleMessage( + db, + threadId: threadId, + threadVariant: threadVariant, + message: message, + using: dependencies + ) } public static func postHandleMessage( _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: Message + message: Message, + using dependencies: Dependencies ) throws { // When handling any message type which has related UI we want to make sure the thread becomes // visible (the only other spot this flag gets set is when sending messages) @@ -424,7 +435,8 @@ public enum MessageReceiver { .updateAllAndConfig( db, SessionThread.Columns.shouldBeVisible.set(to: true), - SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority), + using: dependencies ) } } diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 5362239f559..b343d7c5acc 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -272,7 +272,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat } /// This method marks a thread as read and depending on the target may also update the interactions within a thread as read - public func markAsRead(target: ReadTarget) { + public func markAsRead(target: ReadTarget, using dependencies: Dependencies) { // Store the logic to mark a thread as read (to paths need to run this) let threadId: String = self.threadId let threadWasMarkedUnread: Bool? = self.threadWasMarkedUnread @@ -286,7 +286,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat .filter(id: threadId) .updateAllAndConfig( db, - SessionThread.Columns.markedAsUnread.set(to: false) + SessionThread.Columns.markedAsUnread.set(to: false), + using: dependencies ) } } @@ -327,14 +328,15 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat threadVariant: threadVariant, isBlocked: threadIsBlocked, isMessageRequest: threadIsMessageRequest - ) + ), + using: dependencies ) } } } /// This method will mark a thread as read - public func markAsUnread() { + public func markAsUnread(using dependencies: Dependencies) { guard self.threadWasMarkedUnread != true else { return } let threadId: String = self.threadId @@ -344,7 +346,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat .filter(id: threadId) .updateAllAndConfig( db, - SessionThread.Columns.markedAsUnread.set(to: true) + SessionThread.Columns.markedAsUnread.set(to: true), + using: dependencies ) } } diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index 5b6f3b752c8..e54bd7705fe 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -605,7 +605,7 @@ public struct ProfileManager { else { try Profile .filter(id: publicKey) - .updateAllAndConfig(db, profileChanges) + .updateAllAndConfig(db, profileChanges, using: dependencies) } } diff --git a/SessionMessagingKit/Utilities/SessionEnvironment.swift b/SessionMessagingKit/Utilities/SessionEnvironment.swift index 8c356391e6c..65e8ee3bac2 100644 --- a/SessionMessagingKit/Utilities/SessionEnvironment.swift +++ b/SessionMessagingKit/Utilities/SessionEnvironment.swift @@ -11,9 +11,6 @@ public class SessionEnvironment { public let windowManager: OWSWindowManager public var isRequestingPermission: Bool - // Note: This property is configured after Environment is created. - public let callManager: Atomic = Atomic(nil) - // Note: This property is configured after Environment is created. public let notificationsManager: Atomic = Atomic(nil) diff --git a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift index 9a02e261e2b..ba74eb5f457 100644 --- a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift @@ -9,7 +9,7 @@ import Nimble @testable import SessionMessagingKit @testable import SessionUtilitiesKit -extension Job: MutableIdentifiable { +extension Job: @retroactive MutableIdentifiable { public mutating func setId(_ id: Int64?) { self.id = id } } @@ -67,7 +67,10 @@ class MessageSendJobSpec: QuickSpec { db, id: "Test1", variant: .contact, - values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), + values: SessionThread.TargetValues( + // False is the default and will mean we don't need libSession loaded + shouldBeVisible: .setTo(false) + ), calledFromConfig: false, using: dependencies ) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index f48291489eb..a12b9c06f85 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -19,7 +19,7 @@ class OpenGroupManagerSpec: QuickSpec { id: 234, serverHash: "TestServerHash", messageUuid: nil, - threadId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + threadId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), authorId: "TestAuthorId", variant: .standardOutgoing, body: "Test", @@ -39,12 +39,12 @@ class OpenGroupManagerSpec: QuickSpec { mostRecentFailureText: nil ) @TestState var testGroupThread: SessionThread! = SessionThread( - id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + id: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), variant: .community, creationDateTimestamp: 0 ) @TestState var testOpenGroup: OpenGroup! = OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -210,6 +210,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- cache data context("cache data") { // MARK: ---- defaults the time since last open to greatestFiniteMagnitude @@ -301,7 +309,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.getOrCreatePoller(for: "testserver") + $0.getOrCreatePoller(for: "http://127.0.0.1") }) expect(mockOGMCache) .to(call(matchingParameters: true) { @@ -437,7 +445,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- when there is a thread for the room and the cache has a poller context("when there is a thread for the room and the cache has a poller") { beforeEach { - mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://127.0.0.1"]) } // MARK: ------ for the no-scheme variant @@ -450,7 +458,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -466,7 +474,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "http://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -482,7 +490,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "https://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -501,7 +509,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -517,7 +525,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "http://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -533,7 +541,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "https://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -552,7 +560,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -568,7 +576,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "http://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -584,7 +592,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "https://testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -688,7 +696,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -708,7 +716,7 @@ class OpenGroupManagerSpec: QuickSpec { .hasExistingOpenGroup( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, using: dependencies ) @@ -720,6 +728,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when adding context("when adding") { beforeEach { @@ -746,7 +762,7 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -756,7 +772,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -773,7 +789,7 @@ class OpenGroupManagerSpec: QuickSpec { .fetchOne(db) } ) - .to(equal(OpenGroup.idFor(roomToken: "testRoom", server: "testServer"))) + .to(equal(OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"))) } // MARK: ---- adds a poller @@ -784,7 +800,7 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -794,7 +810,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -804,7 +820,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.getOrCreatePoller(for: "testserver") + $0.getOrCreatePoller(for: "http://127.0.0.1") }) expect(mockPoller) .to(call(matchingParameters: true) { [dependencies = dependencies!] in @@ -815,7 +831,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- an existing room context("an existing room") { beforeEach { - mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://127.0.0.1"]) mockStorage.write { db in try testOpenGroup.insert(db) } @@ -829,7 +845,7 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey .replacingOccurrences(of: "c3", with: "00") .replacingOccurrences(of: "b3", with: "00"), @@ -841,7 +857,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey .replacingOccurrences(of: "c3", with: "00") .replacingOccurrences(of: "b3", with: "00"), @@ -896,7 +912,7 @@ class OpenGroupManagerSpec: QuickSpec { .add( db, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -906,7 +922,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.performInitialRequestsAfterAdd( successfullyAddedGroup: successfullyAddedGroup, roomToken: "testRoom", - server: "testServer", + server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies @@ -934,7 +950,7 @@ class OpenGroupManagerSpec: QuickSpec { .updateAll( db, Interaction.Columns.threadId - .set(to: OpenGroup.idFor(roomToken: "testRoom", server: "testServer")) + .set(to: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1")) ) } } @@ -945,7 +961,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -961,7 +977,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -979,13 +995,13 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) } - expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemovePoller(for: "testserver") }) + expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemovePoller(for: "http://127.0.0.1") }) } // MARK: ------ removes the open group @@ -994,7 +1010,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -1012,7 +1028,7 @@ class OpenGroupManagerSpec: QuickSpec { try OpenGroup.deleteAll(db) try testOpenGroup.insert(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom1", publicKey: TestConstants.publicKey, isActive: true, @@ -1035,7 +1051,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager .delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), calledFromConfigHandling: true, // Don't trigger LibSession logic using: dependencies ) @@ -1133,7 +1149,7 @@ class OpenGroupManagerSpec: QuickSpec { .handleCapabilities( db, capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []), - on: "testserver" + on: "http://127.0.0.1" ) } } @@ -1148,6 +1164,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when handling room poll info context("when handling room poll info") { beforeEach { @@ -1181,7 +1205,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies )// { didComplete = true } } @@ -1206,7 +1230,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) { didCallComplete = true } } @@ -1224,7 +1248,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) { didCallComplete = true } @@ -1240,7 +1264,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -1254,7 +1278,7 @@ class OpenGroupManagerSpec: QuickSpec { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ - OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Just(Data()).setFailureType(to: Error.self).eraseToAnyPublisher() + OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): Just(Data()).setFailureType(to: Error.self).eraseToAnyPublisher() ]) mockStorage.write { db in @@ -1263,7 +1287,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) { didCallComplete = true } @@ -1293,7 +1317,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1303,7 +1327,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1311,7 +1335,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestMod", role: .moderator, @@ -1339,7 +1363,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1349,7 +1373,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1357,7 +1381,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestMod2", role: .moderator, @@ -1379,7 +1403,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1410,7 +1434,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1420,7 +1444,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1428,7 +1452,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestAdmin", role: .admin, @@ -1456,7 +1480,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1466,7 +1490,7 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember .filter(GroupMember.Columns.groupId == OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" )) .fetchOne(db) } @@ -1474,7 +1498,7 @@ class OpenGroupManagerSpec: QuickSpec { GroupMember( groupId: OpenGroup.idFor( roomToken: "testRoom", - server: "testServer" + server: "http://127.0.0.1" ), profileId: "TestAdmin2", role: .admin, @@ -1497,7 +1521,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1521,7 +1545,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1540,7 +1564,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: nil, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1573,7 +1597,7 @@ class OpenGroupManagerSpec: QuickSpec { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ - OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Just(imageData).setFailureType(to: Error.self).eraseToAnyPublisher() + OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): Just(imageData).setFailureType(to: Error.self).eraseToAnyPublisher() ]) } @@ -1595,7 +1619,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1624,7 +1648,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -1648,7 +1672,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1677,7 +1701,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: "testServer", + server: "http://127.0.0.1", roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -1713,7 +1737,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1746,7 +1770,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1766,7 +1790,7 @@ class OpenGroupManagerSpec: QuickSpec { it("does nothing if it fails to retrieve the room image") { mockOGMCache.when { $0.groupImagePublishers } .thenReturn([ - OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Fail(error: NetworkError.unknown).eraseToAnyPublisher() + OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): Fail(error: NetworkError.unknown).eraseToAnyPublisher() ]) testPollInfo = OpenGroupAPI.RoomPollInfo.mockValue.with( @@ -1785,7 +1809,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1819,7 +1843,7 @@ class OpenGroupManagerSpec: QuickSpec { pollInfo: testPollInfo, publicKey: TestConstants.publicKey, for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", waitForImageToComplete: true, using: dependencies ) @@ -1840,6 +1864,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when handling messages context("when handling messages") { beforeEach { @@ -1872,7 +1904,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1894,7 +1926,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1935,7 +1967,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1969,7 +2001,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -1984,7 +2016,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testMessage], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2015,7 +2047,7 @@ class OpenGroupManagerSpec: QuickSpec { testMessage, ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2055,7 +2087,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2085,7 +2117,7 @@ class OpenGroupManagerSpec: QuickSpec { ) ], for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2132,7 +2164,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2166,7 +2198,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2205,7 +2237,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2228,7 +2260,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2270,7 +2302,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2285,7 +2317,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2310,7 +2342,7 @@ class OpenGroupManagerSpec: QuickSpec { testDirectMessage ], fromOutbox: false, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2334,7 +2366,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2355,7 +2387,7 @@ class OpenGroupManagerSpec: QuickSpec { try BlindedIdLookup( blindedId: "15\(TestConstants.publicKey)", sessionId: "TestSessionId", - openGroupServer: "testserver", + openGroupServer: "http://127.0.0.1", openGroupPublicKey: "05\(TestConstants.publicKey)" ).insert(db) } @@ -2365,7 +2397,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2381,7 +2413,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2430,7 +2462,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2445,7 +2477,7 @@ class OpenGroupManagerSpec: QuickSpec { db, messages: [testDirectMessage], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2470,7 +2502,7 @@ class OpenGroupManagerSpec: QuickSpec { testDirectMessage ], fromOutbox: true, - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) } @@ -2483,6 +2515,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { + beforeEach { + LibSession.loadState( + userPublicKey: "05\(TestConstants.publicKey)", + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)), + using: dependencies + ) + } + // MARK: -- when determining if a user is a moderator or an admin context("when determining if a user is a moderator or an admin") { beforeEach { @@ -2497,7 +2537,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2509,7 +2549,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2519,7 +2559,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the key is in the moderator set") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .moderator, isHidden: false @@ -2530,7 +2570,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2540,7 +2580,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the key is in the admin set") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .admin, isHidden: false @@ -2551,7 +2591,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2561,7 +2601,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the moderator is hidden") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .moderator, isHidden: true @@ -2572,7 +2612,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2582,7 +2622,7 @@ class OpenGroupManagerSpec: QuickSpec { it("returns true if the admin is hidden") { mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(TestConstants.publicKey)", role: .admin, isHidden: true @@ -2593,7 +2633,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2605,7 +2645,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "InvalidValue", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2626,7 +2666,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2638,7 +2678,7 @@ class OpenGroupManagerSpec: QuickSpec { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "00\(otherKey)", role: .moderator, isHidden: false @@ -2652,7 +2692,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2671,7 +2711,7 @@ class OpenGroupManagerSpec: QuickSpec { ) mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "15\(otherKey)", role: .moderator, isHidden: false @@ -2682,7 +2722,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "05\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2702,7 +2742,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2721,7 +2761,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2733,7 +2773,7 @@ class OpenGroupManagerSpec: QuickSpec { mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(otherKey)") mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(otherKey)", role: .moderator, isHidden: false @@ -2749,7 +2789,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2768,7 +2808,7 @@ class OpenGroupManagerSpec: QuickSpec { ) mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "15\(otherKey)", role: .moderator, isHidden: false @@ -2784,7 +2824,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "00\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2804,7 +2844,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2820,7 +2860,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2842,7 +2882,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beFalse()) @@ -2862,7 +2902,7 @@ class OpenGroupManagerSpec: QuickSpec { ) mockStorage.write { db in try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "05\(otherKey)", role: .moderator, isHidden: false @@ -2878,7 +2918,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -2898,7 +2938,7 @@ class OpenGroupManagerSpec: QuickSpec { let otherKey: String = TestConstants.publicKey.replacingOccurrences(of: "7", with: "6") try GroupMember( - groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"), profileId: "00\(otherKey)", role: .moderator, isHidden: false @@ -2914,7 +2954,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.isUserModeratorOrAdmin( "15\(TestConstants.publicKey)", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", using: dependencies ) ).to(beTrue()) @@ -3183,14 +3223,14 @@ class OpenGroupManagerSpec: QuickSpec { .eraseToAnyPublisher() mockOGMCache .when { $0.groupImagePublishers } - .thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): publisher]) + .thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): publisher]) var result: Data? OpenGroupManager .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3206,7 +3246,7 @@ class OpenGroupManagerSpec: QuickSpec { .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3216,7 +3256,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.read { db -> Data? in try OpenGroup .select(.imageData) - .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer")) + .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1")) .asRequest(of: Data.self) .fetchOne(db) } @@ -3229,7 +3269,7 @@ class OpenGroupManagerSpec: QuickSpec { .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3250,7 +3290,7 @@ class OpenGroupManagerSpec: QuickSpec { .roomImage( fileId: "1", for: "testRoom", - on: "testServer", + on: "http://127.0.0.1", existingData: nil, using: dependencies ) @@ -3258,7 +3298,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.groupImagePublishers = [OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): publisher] + $0.groupImagePublishers = [OpenGroup.idFor(roomToken: "testRoom", server: "http://127.0.0.1"): publisher] }) } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 742a82be716..3ab4d3c1ec4 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -199,7 +199,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension db, threadId: threadId, threadVariant: threadVariant, - message: messageInfo.message + message: messageInfo.message, + using: dependencies ) return self?.handleSuccessForIncomingCall(db, for: callMessage) @@ -210,7 +211,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension db, threadId: threadId, threadVariant: threadVariant, - message: messageInfo.message + message: messageInfo.message, + using: dependencies ) case .standard(let threadId, let threadVariant, let proto, let messageInfo): diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 94cee744bce..8c9e9add1cc 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -274,7 +274,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView .updateAllAndConfig( db, SessionThread.Columns.shouldBeVisible.set(to: true), - SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority), + using: dependencies ) } From 54c52a5155f3b5ac399799265493ab9fcbee2f70 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Nov 2024 10:37:01 +1100 Subject: [PATCH 06/33] Removed some duplicate code --- .../Database/Models/SessionThread.swift | 35 ++----------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 66aa787a5b5..d1f58bfa455 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -448,22 +448,8 @@ public extension SessionThread { switch type { case .hideContactConversation: - // We need to custom handle the 'Note to Self' conversation - if threadIds.contains(currentUserPublicKey) { - _ = try SessionThread - .filter(id: currentUserPublicKey) - .updateAllAndConfig( - db, - SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false), - calledFromConfig: calledFromConfigHandling, - using: dependencies - ) - } - - // Update any other threads to be hidden _ = try SessionThread - .filter(ids: remainingThreadIds) + .filter(ids: threadIds) .updateAllAndConfig( db, SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), @@ -478,24 +464,9 @@ public extension SessionThread { .filter(threadIds.contains(Interaction.Columns.threadId)) .deleteAll(db) - // We need to custom handle the 'Note to Self' conversation (it should just be - // hidden rather than deleted) - if threadIds.contains(currentUserPublicKey) { - _ = try SessionThread - .filter(id: currentUserPublicKey) - .updateAllAndConfig( - db, - SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), - SessionThread.Columns.shouldBeVisible.set(to: false), - calledFromConfig: calledFromConfigHandling, - using: dependencies - ) - } - - // Update any other threads to be hidden (don't want to actually delete the thread - // record in case it's settings get changed while it's not visible) + // Hide the threads _ = try SessionThread - .filter(ids: remainingThreadIds) + .filter(ids: threadIds) .updateAllAndConfig( db, SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), From 68284315a6f0c314370c6a69b27e362fcdb86fb0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Nov 2024 11:40:16 +1100 Subject: [PATCH 07/33] Fixed some issues with message deletion and the input field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed an issue where the input view wouldn't reappear when deleting messages for everyone in a one-to-one conversation • Fixed an issue where the input view would be visible while the loading indicator was visible when deleting from a community • Fixed an issue where notifications weren't being removed after processing an UnsendRequest • Fixed an issue where legacy groups had a "Clear for everyone" option but it didn't do anything • Updated the copy for legacy groups to say "Delete for everyone" instead of "Clear for everyone" --- .../ConversationVC+Interaction.swift | 35 +++++--- .../Database/Models/Interaction.swift | 79 +++++++++++++------ .../MessageReceiver+UnsendRequests.swift | 56 ++++++++++--- .../Sending & Receiving/MessageReceiver.swift | 3 +- 4 files changed, 123 insertions(+), 50 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ba1669fcb97..a6e02db32ef 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -2109,6 +2109,7 @@ extension ConversationVC: } // Delete the message from the open group + self.hideInputAccessoryView() deleteRemotely( from: self, request: Storage.shared @@ -2133,13 +2134,14 @@ extension ConversationVC: userPublicKey : cellViewModel.threadId ) - let serverHash: String? = Storage.shared.read { db -> String? in - try Interaction - .select(.serverHash) - .filter(id: cellViewModel.id) - .asRequest(of: String.self) - .fetchOne(db) - } + let serverHashes: Set = Storage.shared + .read { db -> Set in + try Interaction.serverHashesForDeletion( + db, + interactionIds: [cellViewModel.id] + ) + } + .defaulting(to: []) let unsendRequest: UnsendRequest = UnsendRequest( timestamp: UInt64(cellViewModel.timestampMs), author: (cellViewModel.variant == .standardOutgoing ? @@ -2153,7 +2155,7 @@ extension ConversationVC: ) // For incoming interactions or interactions with no serverHash just delete them locally - guard cellViewModel.variant == .standardOutgoing, let serverHash: String = serverHash else { + guard cellViewModel.variant == .standardOutgoing, !serverHashes.isEmpty else { Storage.shared.writeAsync { db in _ = try Interaction .filter(id: cellViewModel.id) @@ -2161,7 +2163,7 @@ extension ConversationVC: // No need to send the unsendRequest if there is no serverHash (ie. the message // was outgoing but never got to the server) - guard serverHash != nil else { return } + guard !serverHashes.isEmpty else { return } MessageSender .send( @@ -2209,7 +2211,6 @@ extension ConversationVC: actionSheet.addAction(UIAlertAction( title: { switch (cellViewModel.threadVariant, cellViewModel.threadId) { - case (.legacyGroup, _), (.group, _): return "clearMessagesForEveryone".localized() case (_, userPublicKey): return "deleteMessageDevicesAll".localized() default: return "deleteMessageEveryone".localized() } @@ -2219,6 +2220,10 @@ extension ConversationVC: ) { [weak self] _ in let completeServerDeletion = { Storage.shared.writeAsync { db in + _ = try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + try MessageSender .send( db, @@ -2228,7 +2233,15 @@ extension ConversationVC: threadVariant: cellViewModel.threadVariant, using: dependencies ) + + /// We should also remove the `SnodeReceivedMessageInfo` entries for the hashes (otherwise we + /// might try to poll for a hash which no longer exists, resulting in fetching the last 14 days of messages) + try SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: Array(serverHashes) + ) } + self?.showInputAccessoryView() } // We can only delete messages on the server for `contact` and `group` conversations @@ -2241,7 +2254,7 @@ extension ConversationVC: request: SnodeAPI .deleteMessages( swarmPublicKey: targetPublicKey, - serverHashes: [serverHash] + serverHashes: Array(serverHashes) ) .map { _ in () } .eraseToAnyPublisher() diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index e87ce2b5c7b..20fe4c9e814 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -799,32 +799,6 @@ public extension Interaction { return "\(threadId)-\(id)" } - func markingAsDeleted() -> Interaction { - return Interaction( - id: id, - serverHash: nil, - messageUuid: messageUuid, - threadId: threadId, - authorId: authorId, - variant: .standardIncomingDeleted, - body: nil, - timestampMs: timestampMs, - receivedAtTimestampMs: receivedAtTimestampMs, - wasRead: true, // Never consider deleted messages unread - hasMention: false, - expiresInSeconds: expiresInSeconds, - expiresStartedAtMs: expiresStartedAtMs, - linkPreviewUrl: nil, - openGroupServerMessageId: openGroupServerMessageId, - openGroupWhisper: openGroupWhisper, - openGroupWhisperMods: openGroupWhisperMods, - openGroupWhisperTo: openGroupWhisperTo, - state: .deleted, - recipientReadTimestampMs: nil, - mostRecentFailureText: nil - ) - } - static func isUserMentioned( _ db: Database, threadId: String, @@ -1146,3 +1120,56 @@ public extension Interaction.State { } } } + +// MARK: - Deletion + +public extension Interaction { + /// When deleting a message we should also delete any reactions which were on the message, so fetch and + /// return those hashes as well + static func serverHashesForDeletion( + _ db: Database, + interactionIds: Set, + additionalServerHashesToRemove: [String] = [] + ) throws -> Set { + let messageHashes: [String] = try Interaction + .filter(ids: interactionIds) + .filter(Interaction.Columns.serverHash != nil) + .select(.serverHash) + .asRequest(of: String.self) + .fetchAll(db) + let reactionHashes: [String] = try Reaction + .filter(interactionIds.contains(Reaction.Columns.interactionId)) + .filter(Reaction.Columns.serverHash != nil) + .select(.serverHash) + .asRequest(of: String.self) + .fetchAll(db) + + return Set(messageHashes + reactionHashes + additionalServerHashesToRemove) + } + + func markingAsDeleted() -> Interaction { + return Interaction( + id: id, + serverHash: nil, + messageUuid: messageUuid, + threadId: threadId, + authorId: authorId, + variant: .standardIncomingDeleted, + body: nil, + timestampMs: timestampMs, + receivedAtTimestampMs: receivedAtTimestampMs, + wasRead: true, // Never consider deleted messages unread + hasMention: false, + expiresInSeconds: expiresInSeconds, + expiresStartedAtMs: expiresStartedAtMs, + linkPreviewUrl: nil, + openGroupServerMessageId: openGroupServerMessageId, + openGroupWhisper: openGroupWhisper, + openGroupWhisperMods: openGroupWhisperMods, + openGroupWhisperTo: openGroupWhisperTo, + state: .deleted, + recipientReadTimestampMs: nil, + mostRecentFailureText: nil + ) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index 8f7d7065c93..e9a010384b6 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -10,7 +10,8 @@ extension MessageReceiver { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, - message: UnsendRequest + message: UnsendRequest, + using dependencies: Dependencies ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -27,7 +28,7 @@ extension MessageReceiver { let interaction: Interaction = maybeInteraction else { return } - // Mark incoming messages as read and remove any of their notifications + /// Mark incoming messages as read and remove any of their notifications if interaction.variant == .standardIncoming { try Interaction.markAsRead( db, @@ -42,16 +43,12 @@ extension MessageReceiver { UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: interaction.notificationIdentifiers) } - if author == message.sender, let serverHash: String = interaction.serverHash { - SnodeAPI - .deleteMessages( - swarmPublicKey: author, - serverHashes: [serverHash] - ) - .subscribe(on: DispatchQueue.global(qos: .background)) - .sinkUntilComplete() - } - + /// Retrieve the hashes which should be deleted first (these will be removed by marking the message as deleted) + let hashes: Set = try Interaction.serverHashesForDeletion( + db, + interactionIds: [interactionId] + ) + switch (interaction.variant, (author == message.sender)) { case (.standardOutgoing, _), (_, false): _ = try interaction.delete(db) @@ -71,5 +68,40 @@ extension MessageReceiver { ) } } + + /// Can't delete from the legacy group swarm so only bother for contact conversations + switch threadVariant { + case .legacyGroup, .group, .community: break + case .contact: + dependencies.storage + .readPublisher { db in + try SnodeAPI.preparedDeleteMessages( + db, + swarmPublicKey: userPublicKey, + serverHashes: Array(hashes), + requireSuccessfulDeletion: false, + using: dependencies + ) + } + .flatMap { $0.send(using: dependencies) } + .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .failure: break + case .finished: + /// Since the server deletion was successful we should also remove the `SnodeReceivedMessageInfo` + /// entries for the hashes (otherwise we might try to poll for a hash which no longer exists, resulting in fetching + /// the last 14 days of messages) + dependencies.storage.writeAsync { db in + try SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: Array(hashes) + ) + } + } + } + ) + } } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 6b699fc8fd1..7dcc5ce45af 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -335,7 +335,8 @@ public enum MessageReceiver { db, threadId: threadId, threadVariant: threadVariant, - message: message + message: message, + using: dependencies ) case let message as CallMessage: From 44a2eb3e9058e71b91208a56ca1281d4c9987b15 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Nov 2024 12:27:10 +1100 Subject: [PATCH 08/33] Removed API hacks which are no longer needed --- SessionSnodeKit/Networking/SnodeAPI.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index 5358ecf0e6c..91cf146ade0 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -413,9 +413,6 @@ public final class SnodeAPI { let sendTimestamp: UInt64 = UInt64(SnodeAPI.currentOffsetTimestampMs()) - // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed - let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length - do { return try SnodeAPI .prepareRequest( @@ -683,9 +680,6 @@ public final class SnodeAPI { .eraseToAnyPublisher() } - // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed - let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length - do { return try SnodeAPI .prepareRequest( From 4f86ea7a71250dcf08cde0103e33d98e5d8ddc0f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 28 Nov 2024 17:02:34 +1100 Subject: [PATCH 09/33] Fixed an incorrect filter and an incorrect function param --- SessionMessagingKit/Database/Models/SessionThread.swift | 2 +- .../Message Handling/MessageReceiver+ClosedGroups.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index d1f58bfa455..1208b8c75e8 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -485,7 +485,7 @@ public extension SessionThread { if threadIds.contains(currentUserPublicKey) { // Clear any interactions for the deleted thread _ = try Interaction - .filter(threadIds.contains(Interaction.Columns.threadId)) + .filter(Interaction.Columns.threadId == currentUserPublicKey) .deleteAll(db) _ = try SessionThread diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index be26329df93..075a0bbcd1a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -164,7 +164,7 @@ extension MessageReceiver { creationDateTimestamp: (TimeInterval(formationTimestampMs) / 1000), shouldBeVisible: .setTo(true) ), - calledFromConfig: false, + calledFromConfig: calledFromConfigHandling, using: dependencies ) let closedGroup: ClosedGroup = try ClosedGroup( From 83a0849f667c50b62d4ec210602a8f93c00b55a8 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 29 Nov 2024 15:39:08 +1100 Subject: [PATCH 10/33] add some more logs for testing and debug --- Session.xcodeproj/project.pbxproj | 4 ++-- Session/Calls/Call Management/SessionCallManager.swift | 2 +- Session/Notifications/PushRegistrationManager.swift | 2 ++ .../Message Handling/MessageReceiver+Calls.swift | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2e50a323b87..ea30ead1a0c 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7816,7 +7816,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.2; + MARKETING_VERSION = 2.8.3; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7887,7 +7887,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.2; + MARKETING_VERSION = 2.8.3; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 7235ec02aed..45427383e40 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -113,7 +113,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.localizedCallerName = callerName - update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) + update.remoteHandle = CXHandle(type: .generic, value: call.sessionId) update.hasVideo = false disableUnsupportedFeatures(callUpdate: update) diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 3987ed35c50..7f3b98ef785 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -346,6 +346,8 @@ public enum PushRegistrationError: Error { call.reportIncomingCallIfNeeded { error in if let error = error { SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") + } else { + SNLog("[Calls] Succeeded to report incoming call to CallKit") } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 127ea94ac28..7249ce74fb9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -109,6 +109,7 @@ extension MessageReceiver { // Ignore pre offer message after the same call instance has been generated if let currentCall: CurrentCallProtocol = callManager.currentCall, currentCall.uuid == message.uuid { + SNLog("[MessageReceiver+Calls] Ignoring pre-offer message for call[\(currentCall.uuid)] instance because it is already active.") return } From 5fb9a6621db5bea4b64696a4a57fbe1e5450a4c7 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 29 Nov 2024 17:21:35 +1100 Subject: [PATCH 11/33] add more logs --- Session.xcodeproj/project.pbxproj | 4 +- .../Settings.bundle/ThirdPartyLicenses.plist | 82 +++++++++++++++++++ .../PushRegistrationManager.swift | 71 ++++++++-------- 3 files changed, 122 insertions(+), 35 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index ea30ead1a0c..6fafaac4729 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7778,7 +7778,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 496; + CURRENT_PROJECT_VERSION = 498; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7849,7 +7849,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 496; + CURRENT_PROJECT_VERSION = 498; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist index 1e94962d94c..6ca29b9545a 100644 --- a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist +++ b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist @@ -985,6 +985,88 @@ SOFTWARE. License The MIT License (MIT) +Copyright (c) 2016 swiftlyfalling (https://github.com/swiftlyfalling) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + Title + session-grdb-swift + + + License + The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + * May you do good and not evil. + * May you find forgiveness for yourself and forgive others. + * May you share freely, never taking more than you give. + + Title + session-grdb-swift + + + License + /* +** LICENSE for the sqlite3 WebAssembly/JavaScript APIs. +** +** This bundle (typically released as sqlite3.js or sqlite3.mjs) +** is an amalgamation of JavaScript source code from two projects: +** +** 1) https://emscripten.org: the Emscripten "glue code" is covered by +** the terms of the MIT license and University of Illinois/NCSA +** Open Source License, as described at: +** +** https://emscripten.org/docs/introducing_emscripten/emscripten_license.html +** +** 2) https://sqlite.org: all code and documentation labeled as being +** from this source are released under the same terms as the sqlite3 +** C library: +** +** 2022-10-16 +** +** The author disclaims copyright to this source code. In place of a +** legal notice, here is a blessing: +** +** * May you do good and not evil. +** * May you find forgiveness for yourself and forgive others. +** * May you share freely, never taking more than you give. +*/ + + Title + session-grdb-swift + + + License + The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + May you do good and not evil. + May you find forgiveness for yourself and forgive others. + May you share freely, never taking more than you give. + + Title + session-grdb-swift + + + License + The MIT License (MIT) + Copyright (c) 2015 ibireme <ibireme@gmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 7f3b98ef785..9b292d5f765 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -297,40 +297,45 @@ public enum PushRegistrationError: Error { LibSession.resumeNetworkAccess() let maybeCall: SessionCall? = Storage.shared.write { db in - let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( - state: (caller == getUserHexEncodedPublicKey(db) ? - .outgoing : - .incoming - ) - ) - - let messageInfoString: String? = { - if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { - return String(data: messageInfoData, encoding: .utf8) - } else { - return "callsIncoming" - .put(key: "name", value: caller) - .localized() - } - }() - - let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: caller, variant: .contact, shouldBeVisible: nil) - - let interaction: Interaction = try Interaction( - messageUuid: uuid, - threadId: thread.id, - threadVariant: thread.variant, - authorId: caller, - variant: .infoCall, - body: messageInfoString, - timestampMs: timestampMs - ) - .withDisappearingMessagesConfiguration(db, threadVariant: thread.variant) - .inserted(db) + var call: SessionCall? = nil - call.callInteractionId = interaction.id + do { + let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .incoming) + SNLog("[Calls] messageInfo done") + + let messageInfoString: String? = { + if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { + return String(data: messageInfoData, encoding: .utf8) + } else { + return "callsIncoming" + .put(key: "name", value: caller) + .localized() + } + }() + SNLog("[Calls] messageInfoString done: \(messageInfoString ?? "nil")") + + call = SessionCall(db, for: caller, uuid: uuid, mode: .answer) + SNLog("[Calls] call instance done") + let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact, shouldBeVisible: nil) + SNLog("[Calls] thread got") + + let interaction: Interaction = try Interaction( + messageUuid: uuid, + threadId: thread.id, + threadVariant: thread.variant, + authorId: caller, + variant: .infoCall, + body: messageInfoString, + timestampMs: timestampMs + ) + .withDisappearingMessagesConfiguration(db, threadVariant: thread.variant) + .inserted(db) + SNLog("[Calls] control message inserted") + + call?.callInteractionId = interaction.id + } catch { + SNLog("[Calls] Failed to creat call due to error: \(error)") + } return call } From 780e262e52f8e94411bb13cc26224a3111c38109 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 2 Dec 2024 10:37:31 +1100 Subject: [PATCH 12/33] fix CallKit crash --- Session.xcodeproj/project.pbxproj | 4 +- Session/Home/HomeVC.swift | 1 + .../PushRegistrationManager.swift | 48 +++++++------------ 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 6fafaac4729..209af1863cc 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7778,7 +7778,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 498; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7849,7 +7849,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 498; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index f9e5e732e5c..cc6a99633ae 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -354,6 +354,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // Start polling if needed (i.e. if the user just created or restored their Session ID) if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate { + SNLog("[HomeVC]: Starting pollers...") appDelegate.startPollersIfNeeded() } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 9b292d5f765..d1ed6c10db9 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -300,39 +300,27 @@ public enum PushRegistrationError: Error { var call: SessionCall? = nil do { - let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .incoming) - SNLog("[Calls] messageInfo done") - - let messageInfoString: String? = { - if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { - return String(data: messageInfoData, encoding: .utf8) - } else { - return "callsIncoming" - .put(key: "name", value: caller) - .localized() - } - }() - SNLog("[Calls] messageInfoString done: \(messageInfoString ?? "nil")") + call = SessionCall( + db, + for: caller, + uuid: uuid, + mode: .answer + ) - call = SessionCall(db, for: caller, uuid: uuid, mode: .answer) - SNLog("[Calls] call instance done") - let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact, shouldBeVisible: nil) - SNLog("[Calls] thread got") + let thread: SessionThread = try SessionThread + .fetchOrCreate( + db, + id: caller, + variant: .contact, + shouldBeVisible: nil + ) - let interaction: Interaction = try Interaction( - messageUuid: uuid, - threadId: thread.id, - threadVariant: thread.variant, - authorId: caller, - variant: .infoCall, - body: messageInfoString, - timestampMs: timestampMs - ) - .withDisappearingMessagesConfiguration(db, threadVariant: thread.variant) - .inserted(db) - SNLog("[Calls] control message inserted") + let interaction: Interaction? = try Interaction + .filter(Interaction.Columns.threadId == thread.id) + .filter(Interaction.Columns.messageUuid == uuid) + .fetchOne(db) - call?.callInteractionId = interaction.id + call?.callInteractionId = interaction?.id } catch { SNLog("[Calls] Failed to creat call due to error: \(error)") } From fc4778e79fa7775fe9b53e21bd5f8db88603e0bf Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 2 Dec 2024 11:09:52 +1100 Subject: [PATCH 13/33] don't start other pollers when app is activated in backgroud --- Session/Home/HomeVC.swift | 2 +- Session/Notifications/PushRegistrationManager.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index cc6a99633ae..554d512183b 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -353,7 +353,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS ) // Start polling if needed (i.e. if the user just created or restored their Session ID) - if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate { + if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, !Singleton.appContext.isInBackground { SNLog("[HomeVC]: Starting pollers...") appDelegate.startPollersIfNeeded() } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index d1ed6c10db9..6358fef633a 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -284,7 +284,8 @@ public enum PushRegistrationError: Error { guard let uuid: String = payload["uuid"] as? String, let caller: String = payload["caller"] as? String, - let timestampMs: Int64 = payload["timestamp"] as? Int64 + let timestampMs: UInt64 = payload["timestamp"] as? UInt64, + TimestampUtils.isWithinOneMinute(timestampMs: timestampMs) else { SessionCallManager.reportFakeCall(info: "Missing payload data") // stringlint:ignore return From bf5db2ab93b5d0e33e5e9ddb6b075ef3be4ab777 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 2 Dec 2024 13:38:21 +1100 Subject: [PATCH 14/33] fix an issue where job runner is not activated when answering calls from CallKit --- Session/Notifications/PushRegistrationManager.swift | 2 ++ SessionUtilitiesKit/JobRunner/JobRunner.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 6358fef633a..c14a2013bbd 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -334,6 +334,8 @@ public enum PushRegistrationError: Error { return } + JobRunner.appDidBecomeActive() + // NOTE: Just start 1-1 poller so that it won't wait for polling group messages (UIApplication.shared.delegate as? AppDelegate)?.startPollersIfNeeded(shouldStartGroupPollers: false) diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 7cefce9f8fa..63ae4c5b5fb 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -1901,6 +1901,7 @@ public extension JobRunner { } static func appDidBecomeActive(using dependencies: Dependencies = Dependencies()) { + SNLog("[JobRunner] appDidBecomeActive") instance.appDidBecomeActive(using: dependencies) } From 58707da75ed3b755cb5a640d146d41a65a12ed7d Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 2 Dec 2024 15:11:13 +1100 Subject: [PATCH 15/33] add more logs --- Session.xcodeproj/project.pbxproj | 4 ++-- Session/Calls/Call Management/SessionCallManager.swift | 1 + Session/Home/HomeVC.swift | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 209af1863cc..fd6ec0b96a3 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7778,7 +7778,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 500; + CURRENT_PROJECT_VERSION = 502; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7849,7 +7849,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 500; + CURRENT_PROJECT_VERSION = 502; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 45427383e40..f7c710a2bdd 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -140,6 +140,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } func handleCallEnded() { + SNLog("[Calls] Call ended.") WebRTCSession.current = nil UserDefaults.sharedLokiProject?[.isCallOngoing] = false UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 554d512183b..ad34563e601 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -353,8 +353,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS ) // Start polling if needed (i.e. if the user just created or restored their Session ID) - if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, !Singleton.appContext.isInBackground { - SNLog("[HomeVC]: Starting pollers...") + if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, Singleton.appContext.isInBackground == false { + SNLog("[HomeVC]: Starting pollers... \(Singleton.appContext.reportedApplicationState)") appDelegate.startPollersIfNeeded() } From 1dddedf3eba50639100724867448c750bdddaf27 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 2 Dec 2024 15:42:55 +1100 Subject: [PATCH 16/33] add logs and fix a case when the call is going with CallKit but the app state is not in background --- Session.xcodeproj/project.pbxproj | 4 ++-- Session/Calls/Call Management/SessionCallManager.swift | 1 + Session/Home/HomeVC.swift | 2 +- SessionUtilitiesKit/General/AppContext.swift | 4 +++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index fd6ec0b96a3..50cfb2fdb30 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7778,7 +7778,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 502; + CURRENT_PROJECT_VERSION = 503; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7849,7 +7849,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 502; + CURRENT_PROJECT_VERSION = 503; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index f7c710a2bdd..045e9eb4c41 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -199,6 +199,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } public static func suspendDatabaseIfCallEndedInBackground() { + SNLog("[Calls] suspendDatabaseIfCallEndedInBackground.") if Singleton.hasAppContext && Singleton.appContext.isInBackground { // FIXME: Initialise the `SessionCallManager` with a dependencies instance let dependencies: Dependencies = Dependencies() diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index ad34563e601..600200e64bd 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -354,7 +354,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // Start polling if needed (i.e. if the user just created or restored their Session ID) if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, Singleton.appContext.isInBackground == false { - SNLog("[HomeVC]: Starting pollers... \(Singleton.appContext.reportedApplicationState)") + SNLog("[HomeVC]: Starting pollers...") appDelegate.startPollersIfNeeded() } diff --git a/SessionUtilitiesKit/General/AppContext.swift b/SessionUtilitiesKit/General/AppContext.swift index 423e8f5de5c..6e0bb9f46d0 100644 --- a/SessionUtilitiesKit/General/AppContext.swift +++ b/SessionUtilitiesKit/General/AppContext.swift @@ -46,7 +46,9 @@ public extension AppContext { var frontmostViewController: UIViewController? { nil } var backgroundTimeRemaining: TimeInterval { 0 } - var isInBackground: Bool { reportedApplicationState == .background } + // Note: .inactive is a strange state, if the app is activated by VOIP and CallKit is up in the front, + // the app will report .inactive state + var isInBackground: Bool { reportedApplicationState == .background || reportedApplicationState == .inactive } var isAppForegroundAndActive: Bool { reportedApplicationState == .active } // MARK: - Paths From 8f10f43494dcdd7c6e09d9c1f5a61d8a7dc0a6dc Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Mon, 2 Dec 2024 16:43:18 +1100 Subject: [PATCH 17/33] further fix on app state issues --- Session.xcodeproj/project.pbxproj | 4 ++-- Session/Calls/Call Management/SessionCallManager.swift | 2 +- Session/Home/HomeVC.swift | 3 +-- SessionUtilitiesKit/General/AppContext.swift | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 50cfb2fdb30..6bb171e14fd 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7778,7 +7778,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 503; + CURRENT_PROJECT_VERSION = 504; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7849,7 +7849,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 503; + CURRENT_PROJECT_VERSION = 504; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 045e9eb4c41..602536c3b03 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -145,7 +145,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { UserDefaults.sharedLokiProject?[.isCallOngoing] = false UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil - if Singleton.hasAppContext && Singleton.appContext.isInBackground { + if Singleton.hasAppContext && Singleton.appContext.isNotInForeground { (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() Log.flush() } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 600200e64bd..5b609d18550 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -353,8 +353,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS ) // Start polling if needed (i.e. if the user just created or restored their Session ID) - if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, Singleton.appContext.isInBackground == false { - SNLog("[HomeVC]: Starting pollers...") + if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate, !Singleton.appContext.isNotInForeground { appDelegate.startPollersIfNeeded() } diff --git a/SessionUtilitiesKit/General/AppContext.swift b/SessionUtilitiesKit/General/AppContext.swift index 6e0bb9f46d0..35dcb7e2921 100644 --- a/SessionUtilitiesKit/General/AppContext.swift +++ b/SessionUtilitiesKit/General/AppContext.swift @@ -46,9 +46,9 @@ public extension AppContext { var frontmostViewController: UIViewController? { nil } var backgroundTimeRemaining: TimeInterval { 0 } - // Note: .inactive is a strange state, if the app is activated by VOIP and CallKit is up in the front, - // the app will report .inactive state - var isInBackground: Bool { reportedApplicationState == .background || reportedApplicationState == .inactive } + // Note: CallKit will make the app state as .inactive + var isInBackground: Bool { reportedApplicationState == .background } + var isNotInForeground: Bool { reportedApplicationState != .active } var isAppForegroundAndActive: Bool { reportedApplicationState == .active } // MARK: - Paths From 22e59b178970b4817814c2be99e1894a5065adc0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 2 Dec 2024 17:27:44 +1100 Subject: [PATCH 18/33] Fixed a crash which could occur when receiving a UserGroups config change --- Session.xcodeproj/project.pbxproj | 14 +- .../ConversationVC+Interaction.swift | 6 +- .../Settings/ThreadSettingsViewModel.swift | 2 +- .../GlobalSearchViewController.swift | 2 +- Session/Meta/SessionApp.swift | 2 +- .../PushRegistrationManager.swift | 2 +- Session/Onboarding/Onboarding.swift | 2 +- Session/Open Groups/JoinOpenGroupVC.swift | 2 +- Session/Utilities/MockDataGenerator.swift | 8 +- .../Migrations/_013_SessionUtilChanges.swift | 2 +- .../Database/Models/SessionThread.swift | 9 +- .../Config Handling/LibSession+Contacts.swift | 4 +- .../Config Handling/LibSession+Shared.swift | 246 ++++++++++-------- .../LibSession+UserGroups.swift | 19 +- .../LibSession+UserProfile.swift | 4 +- .../LibSession/Types/Config.swift | 28 ++ .../Open Groups/OpenGroupManager.swift | 10 +- .../MessageReceiver+Calls.swift | 4 +- .../MessageReceiver+ClosedGroups.swift | 10 +- .../MessageReceiver+MessageRequests.swift | 2 +- .../MessageReceiver+VisibleMessages.swift | 2 +- .../MessageSender+ClosedGroups.swift | 6 +- .../Jobs/Types/MessageSendJobSpec.swift | 2 +- .../Open Groups/OpenGroupManagerSpec.swift | 24 +- .../NotificationServiceExtension.swift | 2 +- 25 files changed, 247 insertions(+), 167 deletions(-) create mode 100644 SessionMessagingKit/LibSession/Types/Config.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 783bb078d3e..827d37d516d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -842,6 +842,7 @@ FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */; }; + FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD652CFD6C4E002CDC71 /* Config.swift */; }; FDC289422C86AB5800020BC2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FDC289412C86AB5800020BC2 /* GRDB */; }; FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */; }; FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; @@ -1966,6 +1967,7 @@ FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = ""; }; FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = ""; }; FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotifyRequest.swift; sourceTree = ""; }; + FDC1BD652CFD6C4E002CDC71 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableIdentifiable.swift; sourceTree = ""; }; FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; @@ -4168,8 +4170,9 @@ FD8ECF7529340F4800C0D1BB /* LibSession */ = { isa = PBXGroup; children = ( - FD2B4B022949886900AB4848 /* Database */, FD8ECF8E29381FB200C0D1BB /* Config Handling */, + FD2B4B022949886900AB4848 /* Database */, + FDC1BD642CFD6C44002CDC71 /* Types */, FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */, ); path = LibSession; @@ -4254,6 +4257,14 @@ path = Types; sourceTree = ""; }; + FDC1BD642CFD6C44002CDC71 /* Types */ = { + isa = PBXGroup; + children = ( + FDC1BD652CFD6C4E002CDC71 /* Config.swift */, + ); + path = Types; + sourceTree = ""; + }; FDC289482C881C5500020BC2 /* LibSession */ = { isa = PBXGroup; children = ( @@ -5997,6 +6008,7 @@ FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, + FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */, FD245C53285065DB00B966DD /* ProximityMonitoringManager.swift in Sources */, FD245C55285065E500B966DD /* OpenGroupManager.swift in Sources */, C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 2f1511f56e0..fc97e35d884 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1273,7 +1273,7 @@ extension ConversationVC: id: sessionId, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) } @@ -1309,7 +1309,7 @@ extension ConversationVC: id: (lookup.sessionId ?? lookup.blindedId), variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ).id } @@ -1766,7 +1766,7 @@ extension ConversationVC: roomToken: room, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .flatMap { successfullyAddedGroup in diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index c95caee7d61..49ce1270281 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -781,7 +781,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi id: userId, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index cac8d947dea..767917c6acb 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -399,7 +399,7 @@ extension GlobalSearchViewController { id: threadId, variant: threadVariant, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) } diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index 0e9b6b38f98..d84ce73639e 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -90,7 +90,7 @@ public struct SessionApp { id: threadId, variant: variant, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 971f9960790..90466834b10 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -318,7 +318,7 @@ public class PushRegistrationManager: NSObject, PKPushRegistryDelegate, PushRegi id: caller, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 551a13d4b36..89e8a80ca32 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -175,7 +175,7 @@ enum Onboarding { id: x25519PublicKey, variant: .contact, values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 808e294a868..5cf0f9d2372 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -206,7 +206,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC roomToken: roomToken, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .flatMap { successfullyAddedGroup in diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index 7f802e4f74c..db822af0a47 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -54,7 +54,7 @@ enum MockDataGenerator { id: "MockDatabaseThread", variant: .contact, values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) @@ -85,7 +85,7 @@ enum MockDataGenerator { id: randomSessionId, variant: .contact, values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) @@ -198,7 +198,7 @@ enum MockDataGenerator { id: randomGroupPublicKey, variant: .legacyGroup, values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) _ = try! ClosedGroup( @@ -329,7 +329,7 @@ enum MockDataGenerator { id: randomGroupPublicKey, variant: .community, values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) _ = try! OpenGroup( diff --git a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift index 2b2f33cf6a9..336768cacd3 100644 --- a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift @@ -248,7 +248,7 @@ enum _013_SessionUtilChanges: Migration { id: userPublicKey, variant: .contact, values: SessionThread.TargetValues(shouldBeVisible: .setTo(false)), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 1208b8c75e8..15f6d722448 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -202,7 +202,7 @@ public extension SessionThread { id: ID, variant: Variant, values: TargetValues, - calledFromConfig: Bool, + calledFromConfig configTriggeringChange: LibSession.Config.Variant?, using dependencies: Dependencies ) throws -> SessionThread { var result: SessionThread @@ -215,6 +215,7 @@ public extension SessionThread { db, threadId: id, threadVariant: variant, + conf: configTriggeringChange?.conf, using: dependencies ) @@ -239,13 +240,14 @@ public extension SessionThread { ) case (_, .useLibSession): // Create and save the config from libSession - guard !calledFromConfig else { throw LibSessionError.invalidConfigAccess } + guard configTriggeringChange == nil else { throw LibSessionError.invalidConfigAccess } try LibSession .disappearingMessagesConfig( db, threadId: id, threadVariant: variant, + conf: configTriggeringChange?.conf, using: dependencies )? .upserted(db) @@ -268,6 +270,7 @@ public extension SessionThread { db, threadId: id, threadVariant: variant, + conf: configTriggeringChange?.conf, using: dependencies ) let libSessionShouldBeVisible: Bool = LibSession.shouldBeVisible(priority: targetPriority) @@ -305,7 +308,7 @@ public extension SessionThread { .updateAllAndConfig( db, requiredChanges, - calledFromConfig: calledFromConfig, + calledFromConfig: (configTriggeringChange != nil), using: dependencies ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 9175c04a0fe..b0c2614a9ca 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -39,7 +39,7 @@ internal extension LibSession { using dependencies: Dependencies ) throws { guard mergeNeedsDump else { return } - guard conf != nil else { throw LibSessionError.nilConfigObject } + guard let conf: UnsafeMutablePointer = conf else { throw LibSessionError.nilConfigObject } // The current users contact data is handled separately so exclude it if it's present (as that's // actually a bug) @@ -178,7 +178,7 @@ internal extension LibSession { .useExisting ) ), - calledFromConfig: true, + calledFromConfig: .contacts(conf), using: dependencies ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 9409148805e..fb84e412ea9 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -410,6 +410,7 @@ public extension LibSession { _ db: Database, threadId: String, threadVariant: SessionThread.Variant, + conf: UnsafeMutablePointer?, using dependencies: Dependencies ) -> Int32 { let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -420,70 +421,81 @@ public extension LibSession { } }() - return dependencies.caches[.libSession] - .config(for: configVariant, publicKey: userPublicKey) - .wrappedValue - .map { conf in - guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { + guard let conf: UnsafeMutablePointer = conf else { + return dependencies.caches[.libSession] + .config(for: configVariant, publicKey: userPublicKey) + .wrappedValue + .map { conf in + LibSession.pinnedPriority( + db, + threadId: threadId, + threadVariant: threadVariant, + conf: conf, + using: dependencies + ) + } + .defaulting(to: LibSession.defaultNewThreadPriority) + } + + guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { + return LibSession.defaultNewThreadPriority + } + + switch threadVariant { + case .contact where threadId == userPublicKey: + return user_profile_get_nts_priority(conf) + + case .contact: + var contact: contacts_contact = contacts_contact() + + guard contacts_get(conf, &contact, &cThreadId) else { + LibSessionError.clear(conf) return LibSession.defaultNewThreadPriority } - switch threadVariant { - case .contact where threadId == userPublicKey: - return user_profile_get_nts_priority(conf) - - case .contact: - var contact: contacts_contact = contacts_contact() - - guard contacts_get(conf, &contact, &cThreadId) else { - LibSessionError.clear(conf) - return LibSession.defaultNewThreadPriority - } - - return contact.priority - - case .community: - let maybeUrlInfo: OpenGroupUrlInfo? = Storage.shared - .read { db in try OpenGroupUrlInfo.fetchAll(db, ids: [threadId]) }? - .first - - guard - let urlInfo: OpenGroupUrlInfo = maybeUrlInfo, - var cBaseUrl: [CChar] = urlInfo.server.cString(using: .utf8), - var cRoom: [CChar] = urlInfo.roomToken.cString(using: .utf8) - else { return LibSession.defaultNewThreadPriority } - - var community: ugroups_community_info = ugroups_community_info() - let result: Bool = user_groups_get_community(conf, &community, &cBaseUrl, &cRoom) - LibSessionError.clear(conf) - - return community.priority - - case .legacyGroup: - let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) - LibSessionError.clear(conf) - - defer { - if groupInfo != nil { - ugroups_legacy_group_free(groupInfo) - } - } - - return (groupInfo?.pointee.priority ?? LibSession.defaultNewThreadPriority) - - case .group: - return LibSession.defaultNewThreadPriority // FIXME: Add in groups rebuild + return contact.priority + + case .community: + let maybeUrlInfo: OpenGroupUrlInfo? = Storage.shared + .read { db in try OpenGroupUrlInfo.fetchAll(db, ids: [threadId]) }? + .first + + guard + let urlInfo: OpenGroupUrlInfo = maybeUrlInfo, + var cBaseUrl: [CChar] = urlInfo.server.cString(using: .utf8), + var cRoom: [CChar] = urlInfo.roomToken.cString(using: .utf8) + else { return LibSession.defaultNewThreadPriority } + + var community: ugroups_community_info = ugroups_community_info() + let result: Bool = user_groups_get_community(conf, &community, &cBaseUrl, &cRoom) + LibSessionError.clear(conf) + + return community.priority + + case .legacyGroup: + let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) + LibSessionError.clear(conf) + + defer { + if groupInfo != nil { + ugroups_legacy_group_free(groupInfo) + } } - } - .defaulting(to: LibSession.defaultNewThreadPriority) + + return (groupInfo?.pointee.priority ?? LibSession.defaultNewThreadPriority) + + case .group: + return LibSession.defaultNewThreadPriority // FIXME: Add in groups rebuild + } } static func disappearingMessagesConfig( _ db: Database, threadId: String, threadVariant: SessionThread.Variant, + conf: UnsafeMutablePointer?, using dependencies: Dependencies - ) throws -> DisappearingMessagesConfiguration? { + ) -> DisappearingMessagesConfiguration? { switch threadVariant { case .community: return nil default: break @@ -497,65 +509,75 @@ public extension LibSession { } }() - return dependencies.caches[.libSession] - .config(for: configVariant, publicKey: userPublicKey) - .wrappedValue - .map { conf -> DisappearingMessagesConfiguration? in - guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { return nil } + guard let conf: UnsafeMutablePointer = conf else { + return dependencies.caches[.libSession] + .config(for: configVariant, publicKey: userPublicKey) + .wrappedValue + .map { conf in + LibSession.disappearingMessagesConfig( + db, + threadId: threadId, + threadVariant: threadVariant, + conf: conf, + using: dependencies + ) + } + } + + guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { return nil } + + switch threadVariant { + case .community: return nil + case .contact where threadId == userPublicKey: + let targetExpiry: Int32 = user_profile_get_nts_expiry(conf) + let targetIsEnabled: Bool = (targetExpiry > 0) - switch threadVariant { - case .community: return nil - case .contact where threadId == userPublicKey: - let targetExpiry: Int32 = user_profile_get_nts_expiry(conf) - let targetIsEnabled: Bool = (targetExpiry > 0) - - return DisappearingMessagesConfiguration( - threadId: threadId, - isEnabled: targetIsEnabled, - durationSeconds: TimeInterval(targetExpiry), - type: targetIsEnabled ? .disappearAfterSend : .unknown - ) - - case .contact: - var contact: contacts_contact = contacts_contact() - - guard contacts_get(conf, &contact, &cThreadId) else { - LibSessionError.clear(conf) - return nil - } - - return DisappearingMessagesConfiguration( - threadId: threadId, - isEnabled: contact.exp_seconds > 0, - durationSeconds: TimeInterval(contact.exp_seconds), - type: DisappearingMessagesConfiguration.DisappearingMessageType( - libSessionType: contact.exp_mode - ) - ) - - case .legacyGroup: - let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) - LibSessionError.clear(conf) - - defer { - if groupInfo != nil { - ugroups_legacy_group_free(groupInfo) - } - } - - return groupInfo.map { info in - DisappearingMessagesConfiguration( - threadId: userPublicKey, - isEnabled: (info.pointee.disappearing_timer > 0), - durationSeconds: TimeInterval(info.pointee.disappearing_timer), - type: .disappearAfterSend - ) - } - - case .group: - return nil // FIXME: Add in groups rebuild + return DisappearingMessagesConfiguration( + threadId: threadId, + isEnabled: targetIsEnabled, + durationSeconds: TimeInterval(targetExpiry), + type: targetIsEnabled ? .disappearAfterSend : .unknown + ) + + case .contact: + var contact: contacts_contact = contacts_contact() + + guard contacts_get(conf, &contact, &cThreadId) else { + LibSessionError.clear(conf) + return nil } - } + + return DisappearingMessagesConfiguration( + threadId: threadId, + isEnabled: contact.exp_seconds > 0, + durationSeconds: TimeInterval(contact.exp_seconds), + type: DisappearingMessagesConfiguration.DisappearingMessageType( + libSessionType: contact.exp_mode + ) + ) + + case .legacyGroup: + let groupInfo: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cThreadId) + LibSessionError.clear(conf) + + defer { + if groupInfo != nil { + ugroups_legacy_group_free(groupInfo) + } + } + + return groupInfo.map { info in + DisappearingMessagesConfiguration( + threadId: userPublicKey, + isEnabled: (info.pointee.disappearing_timer > 0), + durationSeconds: TimeInterval(info.pointee.disappearing_timer), + type: .disappearAfterSend + ) + } + + case .group: + return nil // FIXME: Add in groups rebuild + } } static func conversationInConfig( @@ -565,8 +587,8 @@ public extension LibSession { visibleOnly: Bool, using dependencies: Dependencies ) -> Bool { - // Currently blinded conversations cannot be contained in the config, so there is no point checking (it'll always be - // false) + // Currently blinded conversations cannot be contained in the config, so there is no + // point checking (it'll always be false) guard threadVariant == .community || ( (try? SessionId(from: threadId))?.prefix != .blinded15 && diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 08797ea0959..af38e10fab6 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -32,7 +32,7 @@ internal extension LibSession { using dependencies: Dependencies = Dependencies() ) throws { guard mergeNeedsDump else { return } - guard conf != nil else { throw LibSessionError.nilConfigObject } + guard let conf: UnsafeMutablePointer = conf else { throw LibSessionError.nilConfigObject } var infiniteLoopGuard: Int = 0 var communities: [PrioritisedData] = [] @@ -133,14 +133,13 @@ internal extension LibSession { // Add any new communities (via the OpenGroupManager) communities.forEach { community in - let successfullyAddedGroup: Bool = OpenGroupManager.shared - .add( - db, - roomToken: community.data.roomToken, - server: community.data.server, - publicKey: community.data.publicKey, - calledFromConfigHandling: true - ) + let successfullyAddedGroup: Bool = OpenGroupManager.shared.add( + db, + roomToken: community.data.roomToken, + server: community.data.server, + publicKey: community.data.publicKey, + calledFromConfig: .userGroups(conf) + ) if successfullyAddedGroup { db.afterNextTransactionNested { _ in @@ -241,7 +240,7 @@ internal extension LibSession { admins: updatedAdmins.map { $0.profileId }, expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0), formationTimestampMs: UInt64(joinedAt * 1000), - calledFromConfigHandling: true, + calledFromConfig: .userGroups(conf), using: dependencies ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 3b152d07120..ac3b04ac50b 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -28,7 +28,7 @@ internal extension LibSession { typealias ProfileData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?) guard mergeNeedsDump else { return } - guard conf != nil else { throw LibSessionError.nilConfigObject } + guard let conf: UnsafeMutablePointer = conf else { throw LibSessionError.nilConfigObject } // A profile must have a name so if this is null then it's invalid and can be ignored guard let profileNamePtr: UnsafePointer = user_profile_get_name(conf) else { return } @@ -106,7 +106,7 @@ internal extension LibSession { shouldBeVisible: .setTo(LibSession.shouldBeVisible(priority: targetPriority)), pinnedPriority: .setTo(targetPriority) ), - calledFromConfig: true, + calledFromConfig: .userProfile(conf), using: dependencies ) } diff --git a/SessionMessagingKit/LibSession/Types/Config.swift b/SessionMessagingKit/LibSession/Types/Config.swift new file mode 100644 index 00000000000..bf0c57ec334 --- /dev/null +++ b/SessionMessagingKit/LibSession/Types/Config.swift @@ -0,0 +1,28 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +public extension LibSession { + // MARK: - Config + + enum Config { + public enum Variant { + case userProfile(UnsafeMutablePointer) + case contacts(UnsafeMutablePointer) + case convoInfoVolatile(UnsafeMutablePointer) + case userGroups(UnsafeMutablePointer) + + var conf: UnsafeMutablePointer { + switch self { + case .userProfile(let value), .contacts(let value), + .convoInfoVolatile(let value), .userGroups(let value): + return value + } + } + } + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index d10cdd34e2c..b4969484b2c 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -180,12 +180,12 @@ public final class OpenGroupManager { roomToken: String, server: String, publicKey: String, - calledFromConfigHandling: Bool, + calledFromConfig configTriggeringChange: LibSession.Config.Variant?, using dependencies: Dependencies = Dependencies() ) -> Bool { // If we are currently polling for this server and already have a TSGroupThread for this room the do nothing if hasExistingOpenGroup(db, roomToken: roomToken, server: server, publicKey: publicKey, using: dependencies) { - SNLog("Ignoring join open group attempt (already joined), user initiated: \(!calledFromConfigHandling)") + SNLog("Ignoring join open group attempt (already joined), user initiated: \(configTriggeringChange != nil)") return false } @@ -211,9 +211,9 @@ public final class OpenGroupManager { /// /// **Note:** We **MUST** provide a `nil` value if this method was called from the config handling as updating /// the `shouldVeVisible` state can trigger a config update which could result in an infinite loop in the future - shouldBeVisible: (calledFromConfigHandling ? .useExisting : .setTo(true)) + shouldBeVisible: (configTriggeringChange != nil ? .useExisting : .setTo(true)) ), - calledFromConfig: calledFromConfigHandling, + calledFromConfig: configTriggeringChange, using: dependencies ) @@ -225,7 +225,7 @@ public final class OpenGroupManager { // Set the group to active and reset the sequenceNumber (handle groups which have // been deactivated) - if calledFromConfigHandling { + if configTriggeringChange != nil { _ = try? OpenGroup .filter(id: OpenGroup.idFor(roomToken: roomToken, server: targetServer)) .updateAll( // Handling a config update so don't use `updateAllAndConfig` diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 55d8686315c..54dcdc0e463 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -65,7 +65,7 @@ extension MessageReceiver { id: sender, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) @@ -92,7 +92,7 @@ extension MessageReceiver { id: sender, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 075a0bbcd1a..79750f55c1c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -121,7 +121,7 @@ extension MessageReceiver { admins: adminsAsData.map { $0.toHexString() }, expirationTimer: expirationTimer, formationTimestampMs: sentTimestamp, - calledFromConfigHandling: false, + calledFromConfig: nil, using: dependencies ) } @@ -135,7 +135,7 @@ extension MessageReceiver { admins: [String], expirationTimer: UInt32, formationTimestampMs: UInt64, - calledFromConfigHandling: Bool, + calledFromConfig configTriggeringChange: LibSession.Config.Variant?, using dependencies: Dependencies ) throws { // With new closed groups we only want to create them if the admin creating the closed group is an @@ -153,7 +153,7 @@ extension MessageReceiver { // If the group came from the updated config handling then it doesn't matter if we // have an approved admin - we should add it regardless (as it's been synced from // antoher device) - guard hasApprovedAdmin || calledFromConfigHandling else { return } + guard hasApprovedAdmin || configTriggeringChange != nil else { return } // Create the group let thread: SessionThread = try SessionThread.upsert( @@ -164,7 +164,7 @@ extension MessageReceiver { creationDateTimestamp: (TimeInterval(formationTimestampMs) / 1000), shouldBeVisible: .setTo(true) ), - calledFromConfig: calledFromConfigHandling, + calledFromConfig: configTriggeringChange, using: dependencies ) let closedGroup: ClosedGroup = try ClosedGroup( @@ -226,7 +226,7 @@ extension MessageReceiver { try newKeyPair.insert(db) } - if !calledFromConfigHandling { + if configTriggeringChange == nil { // Update libSession try? LibSession.add( db, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index e83796f4a49..e4a12fd4d74 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -55,7 +55,7 @@ extension MessageReceiver { id: senderId, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index ef9a21b7aae..0dd8f4c6fb7 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -72,7 +72,7 @@ extension MessageReceiver { id: threadId, variant: threadVariant, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) let maybeOpenGroup: OpenGroup? = { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index 05d7b56a14d..2d08ddc964b 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -45,7 +45,7 @@ extension MessageSender { id: groupPublicKey, variant: .legacyGroup, values: SessionThread.TargetValues(shouldBeVisible: .setTo(true)), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) try ClosedGroup( @@ -483,7 +483,7 @@ extension MessageSender { id: member, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) @@ -697,7 +697,7 @@ extension MessageSender { id: publicKey, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) let ciphertext = try dependencies.crypto.tryGenerate( diff --git a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift index ba74eb5f457..d3e231d3f3d 100644 --- a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift @@ -71,7 +71,7 @@ class MessageSendJobSpec: QuickSpec { // False is the default and will mean we don't need libSession loaded shouldBeVisible: .setTo(false) ), - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) }, diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index a12b9c06f85..39c5a57bf2b 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -764,7 +764,11 @@ class OpenGroupManagerSpec: QuickSpec { roomToken: "testRoom", server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } @@ -802,7 +806,11 @@ class OpenGroupManagerSpec: QuickSpec { roomToken: "testRoom", server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } @@ -849,7 +857,11 @@ class OpenGroupManagerSpec: QuickSpec { publicKey: TestConstants.serverPublicKey .replacingOccurrences(of: "c3", with: "00") .replacingOccurrences(of: "b3", with: "00"), - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } @@ -914,7 +926,11 @@ class OpenGroupManagerSpec: QuickSpec { roomToken: "testRoom", server: "http://127.0.0.1", publicKey: TestConstants.serverPublicKey, - calledFromConfigHandling: true, // Don't trigger LibSession logic + calledFromConfig: .userGroups( // Don't trigger LibSession logic + dependencies.caches[.libSession] + .config(for: .userGroups, publicKey: "05\(TestConstants.publicKey)") + .wrappedValue! + ), using: dependencies ) } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 3ab4d3c1ec4..39d31ebd0d8 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -169,7 +169,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension id: sender, variant: .contact, values: .existingOrDefault, - calledFromConfig: false, + calledFromConfig: nil, using: dependencies ) From 35758b29462d618313c8382d7160cb82a144700e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 3 Dec 2024 17:46:04 +1100 Subject: [PATCH 19/33] Added logic for dev setting import/export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added the dev settings UI • Added export and import functionality (import is unfinished) • Updated the export logs to allow for either sharing or saving direct to files --- Session.xcodeproj/project.pbxproj | 8 + .../Settings/DeveloperSettingsViewModel.swift | 457 ++++++++++++++++++ Session/Settings/HelpViewModel.swift | 291 ++++------- Session/Settings/SettingsViewModel.swift | 456 +++++++++-------- .../Settings/Views/VersionFooterView.swift | 33 +- .../Shared/Types/ObservableTableSource.swift | 97 +++- .../Database/Models/Attachment.swift | 2 +- .../Utilities/Preferences.swift | 4 + .../Components/ConfirmationModal.swift | 83 ++++ SessionUIKit/Components/Modal.swift | 11 +- SessionUtilitiesKit/Database/Storage.swift | 14 +- .../Utilities/DirectoryArchiver.swift | 387 +++++++++++++++ ...ModalActivityIndicatorViewController.swift | 57 ++- 13 files changed, 1422 insertions(+), 478 deletions(-) create mode 100644 Session/Settings/DeveloperSettingsViewModel.swift create mode 100644 SessionUtilitiesKit/Utilities/DirectoryArchiver.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2e50a323b87..c929c70e189 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -840,6 +840,8 @@ FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */; }; + FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */; }; + FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */; }; FDC289422C86AB5800020BC2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FDC289412C86AB5800020BC2 /* GRDB */; }; FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */; }; FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; @@ -1962,6 +1964,8 @@ FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = ""; }; FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = ""; }; FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotifyRequest.swift; sourceTree = ""; }; + FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsViewModel.swift; sourceTree = ""; }; + FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryArchiver.swift; sourceTree = ""; }; FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableIdentifiable.swift; sourceTree = ""; }; FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; @@ -3101,6 +3105,7 @@ FD37E9CB28A1E578003AE748 /* AppearanceViewController.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, + FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); @@ -3614,6 +3619,7 @@ FD6A39272C2AB2AA00762359 /* CGSize+Utilities.swift */, FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, FD12A84A2AD6458800EEBA0D /* DifferenceKit+Utilities.swift */, + FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */, FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */, FD09796A27F6C67500936362 /* Failable.swift */, FD6A39092C2A8F2D00762359 /* FileManagerType.swift */, @@ -5815,6 +5821,7 @@ FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */, FD428B212B4B75EA006D0888 /* Singleton.swift in Sources */, FD6A39082C2A8DDA00762359 /* FileSystem.swift in Sources */, + FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, FD8ECF922938552800C0D1BB /* Threading.swift in Sources */, FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */, @@ -6243,6 +6250,7 @@ B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */, FD12A8472AD63C3400EEBA0D /* PagedObservationSource.swift in Sources */, B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */, + FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */, 7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */, C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */, 942256822C23F8BB00C0FDBF /* InviteAFriendScreen.swift in Sources */, diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift new file mode 100644 index 00000000000..9b2572427b8 --- /dev/null +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -0,0 +1,457 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import CryptoKit +import Compression +import GRDB +import DifferenceKit +import SessionUIKit +import SessionSnodeKit +import SessionMessagingKit +import SessionUtilitiesKit +import SignalUtilitiesKit + +class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + private var databaseKeyEncryptionPassword: String = "" + private var documentPickerResult: DocumentPickerResult? + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + // MARK: - Section + + public enum Section: SessionTableSection { + case developerMode + case database + + var title: String? { + switch self { + case .developerMode: return nil + case .database: return "Database" + } + } + + var style: SessionTableSectionStyle { + switch self { + case .developerMode: return .padding + default: return .titleRoundedContent + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case developerMode + + case exportDatabase + case importDatabase + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .developerMode: return "developerMode" + + case .exportDatabase: return "exportDatabase" + case .importDatabase: return "importDatabase" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.developerMode { + case .developerMode: result.append(.developerMode); fallthrough + + case .exportDatabase: result.append(.exportDatabase); fallthrough + case .importDatabase: result.append(.importDatabase) + } + + return result + } + } + + // MARK: - Content + + private struct State: Equatable { + let developerMode: Bool + } + + let title: String = "Developer Settings" + + lazy var observation: TargetObservation = ObservationBuilder + .refreshableData(self) { [weak self, dependencies] () -> State in + State( + developerMode: dependencies.storage[.developerModeEnabled] + ) + } + .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } + + private func content(_ previous: State?, _ current: State) -> [SectionModel] { + return [ + SectionModel( + model: .developerMode, + elements: [ + SessionCell.Info( + id: .developerMode, + title: "Developer Mode", + subtitle: """ + Grants access to this screen. + + Disabling this setting will: + • Reset all the below settings to default (removing data as described below) + • Revoke access to this screen unless Developer Mode is re-enabled + """, + rightAccessory: .toggle( + .boolValue( + current.developerMode, + oldValue: (previous?.developerMode == true) + ) + ), + onTap: { [weak self] in + guard current.developerMode else { return } + + self?.disableDeveloperMode() + } + ) + ] + ), + SectionModel( + model: .database, + elements: [ + SessionCell.Info( + id: .exportDatabase, + title: "Export App Data", + rightAccessory: .icon( + UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")? + .withRenderingMode(.alwaysTemplate), + size: .small + ), + styling: SessionCell.StyleInfo( + tintColor: .danger + ), + onTapView: { [weak self] view in self?.exportDatabase(view) } + ), + SessionCell.Info( + id: .importDatabase, + title: "Import App Data", + rightAccessory: .icon( + UIImage(systemName: "square.and.arrow.down")? + .withRenderingMode(.alwaysTemplate), + size: .small + ), + styling: SessionCell.StyleInfo( + tintColor: .danger + ), + onTapView: { [weak self] view in self?.importDatabase(view) } + ) + ] + ) + ] + } + + // MARK: - Functions + + private func disableDeveloperMode() { + /// Loop through all of the sections and reset the features back to default for each one as needed (this way if a new section is added + /// then we will get a compile error if it doesn't get resetting instructions added) + TableItem.allCases.forEach { item in + switch item { + case .developerMode: break // Not a feature + + case .exportDatabase: break // Not a feature + case .importDatabase: break // Not a feature + } + } + + /// Disable developer mode + dependencies.storage.write { db in + db[.developerModeEnabled] = false + } + + self.dismissScreen(type: .pop) + } + + // MARK: - Export and Import + + private func exportDatabase(_ targetView: UIView?) { + let generatedPassword: String = UUID().uuidString + self.databaseKeyEncryptionPassword = generatedPassword + + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Export App Data", + body: .input( + explanation: NSAttributedString( + string: """ + This will generate a file encrypted using the provided password includes all app data, attachments, settings and keys. + + We've generated a secure password for you but feel free to provide your own. + + Use at your own risk! + """ + ), + placeholder: "Enter a password", + initialValue: generatedPassword, + clearButton: true, + onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } + ), + confirmTitle: "save".localized(), + confirmStyle: .alert_text, + cancelTitle: "share".localized(), + cancelStyle: .alert_text, + hasCloseButton: true, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + modal.dismiss(animated: true) { + self?.performExport(viaShareSheet: false, targetView: targetView) + } + }, + onCancel: { [weak self] modal in + modal.dismiss(animated: true) { + self?.performExport(viaShareSheet: true, targetView: targetView) + } + } + ) + ), + transitionType: .present + ) + } + + private func importDatabase(_ targetView: UIView?) { + func showError(_ error: Error) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: { + switch error { + case CryptoKitError.incorrectKeySize: + return .text("The password must be between 6 and 32 characters (padded to 32 bytes)") + + default: return .text("Failed to export database") + } + }() + ) + ), + transitionType: .present + ) + } + + self.databaseKeyEncryptionPassword = "" + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Import App Data", + body: .input( + explanation: NSAttributedString( + string: """ + Importing a database will result in the loss of all data stored locally. + + Use at your own risk! + """ + ), + placeholder: "Enter a password", + initialValue: "", + clearButton: true, + onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } + ), + confirmTitle: "Import", + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + modal.dismiss(animated: true) { + guard + let password: String = self?.databaseKeyEncryptionPassword, + password.count >= 6 + else { return showError(CryptoKitError.incorrectKeySize) } + + let documentPickerResult: DocumentPickerResult = DocumentPickerResult { url in + guard let url: URL = url else { return } + + let viewController: UIViewController = ModalActivityIndicatorViewController(canCancel: false) { modalActivityIndicator in + do { + let tmpUnencryptPath: String = "\(Singleton.appContext.temporaryDirectory)/new_session.bak" + let extraFilePaths: [String] = try DirectoryArchiver.unarchiveDirectory( + archivePath: url.path, + destinationPath: tmpUnencryptPath, + password: password, + progressChanged: { fileProgress, fileSize in + let percentage: Int = { + guard fileSize > 0 else { return 0 } + + return Int((Double(fileProgress) / Double(fileSize)) * 100) + }() + + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Decryption progress: \(percentage)%" + ) + } + } + ) + + // TODO: Need to actually replace the current content then kill the app + // TODO: Might be nice to validate that we have database access to the new database with the key + print("RAWR") + modalActivityIndicator.dismiss { + print("RAWR2") + } + } + catch { showError(error) } + } + + self?.transitionToScreen(viewController, transitionType: .present) + } + self?.documentPickerResult = documentPickerResult + + // UIDocumentPickerModeImport copies to a temp file within our container. + // It uses more memory than "open" but lets us avoid working with security scoped URLs. + let documentPickerVC = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true) + documentPickerVC.delegate = documentPickerResult + documentPickerVC.modalPresentationStyle = .fullScreen + + self?.transitionToScreen(documentPickerVC, transitionType: .present) + } + } + ) + ), + transitionType: .present + ) + } + + private func performExport( + viaShareSheet: Bool, + targetView: UIView? + ) { + func showError(_ error: Error) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: { + switch error { + case CryptoKitError.incorrectKeySize: + return .text("The password must be between 6 and 32 characters (padded to 32 bytes)") + + default: return .text("Failed to export database") + } + }() + ) + ), + transitionType: .present + ) + } + guard databaseKeyEncryptionPassword.count >= 6 else { return showError(CryptoKitError.incorrectKeySize) } + guard Singleton.hasAppContext else { return showError(CryptoKitError.incorrectParameterSize) } + + let viewController: UIViewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, databaseKeyEncryptionPassword, dependencies] modalActivityIndicator in + let backupFile: String = "\(Singleton.appContext.temporaryDirectory)/session.bak" + + do { + let secureDbKey: String = try dependencies.storage.secureExportKey( + password: databaseKeyEncryptionPassword + ) + + try DirectoryArchiver.archiveDirectory( + sourcePath: FileManager.default.appSharedDataDirectoryPath, + destinationPath: backupFile, + additionalPaths: [secureDbKey], + password: databaseKeyEncryptionPassword, + progressChanged: { fileIndex, totalFiles, currentFileProgress, currentFileSize in + let percentage: Int = { + guard currentFileSize > 0 else { return 100 } + + let percentage: Int = Int((Double(currentFileProgress) / Double(currentFileSize)) * 100) + + guard percentage > 0 else { return 100 } + + return percentage + }() + + DispatchQueue.main.async { + modalActivityIndicator.setMessage([ + "Exporting file: \(fileIndex)/\(totalFiles)", + "File encryption progress: \(percentage)%" + ].compactMap { $0 }.joined(separator: "\n")) + } + } + ) + } + catch { return showError(error) } + + modalActivityIndicator.dismiss { + switch viaShareSheet { + case true: + let shareVC: UIActivityViewController = UIActivityViewController( + activityItems: [ URL(fileURLWithPath: backupFile) ], + applicationActivities: nil + ) + shareVC.completionWithItemsHandler = { _, _, _, _ in } + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + shareVC.popoverPresentationController?.sourceView = targetView + shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) + } + + self?.transitionToScreen(shareVC, transitionType: .present) + + case false: + // Create and present the document picker + let documentPickerResult: DocumentPickerResult = DocumentPickerResult { _ in } + self?.documentPickerResult = documentPickerResult + + let documentPicker: UIDocumentPickerViewController = UIDocumentPickerViewController( + forExporting: [URL(fileURLWithPath: backupFile)] + ) + documentPicker.delegate = documentPickerResult + documentPicker.modalPresentationStyle = .formSheet + self?.transitionToScreen(documentPicker, transitionType: .present) + } + } + } + + self.transitionToScreen(viewController, transitionType: .present) + } +} + +private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { + private let onResult: (URL?) -> Void + + init(onResult: @escaping (URL?) -> Void) { + self.onResult = onResult + } + + // MARK: - UIDocumentPickerDelegate + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url: URL = urls.first else { + self.onResult(nil) + return + } + + self.onResult(url) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + self.onResult(nil) + } +} diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index e8fff044134..31eb04bb366 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -15,10 +15,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa public let navigatableState: NavigatableState = NavigatableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() - -#if DEBUG - private var databaseKeyEncryptionPassword: String = "" -#endif + private static var documentPickerResult: DocumentPickerResult? // MARK: - Initialization @@ -34,9 +31,6 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa case feedback case faq case support -#if DEBUG - case exportDatabase -#endif var style: SessionTableSectionStyle { .padding } } @@ -147,33 +141,9 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa } ) ] - ), - maybeExportDbSection + ) ] -#if DEBUG - private lazy var maybeExportDbSection: SectionModel? = SectionModel( - model: .exportDatabase, - elements: [ - SessionCell.Info( - id: .support, - title: "Export Database", // stringlint:ignore - rightAccessory: .icon( - UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")? - .withRenderingMode(.alwaysTemplate), - size: .small - ), - styling: SessionCell.StyleInfo( - tintColor: .danger - ), - onTapView: { [weak self] view in self?.exportDatabase(view) } - ) - ] - ) -#else - private let maybeExportDbSection: SectionModel? = nil -#endif - // MARK: - Functions public static func shareLogs( @@ -188,41 +158,54 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa let viewController: UIViewController = Singleton.appContext.frontmostViewController else { return } - #if targetEnvironment(simulator) - // stringlint:ignore_start let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( - title: "Export Logs", - body: .text( - "How would you like to export the logs?\n\n(This modal only appears on the Simulator)" - ), - confirmTitle: "Copy Path", - cancelTitle: "Share", + title: "helpReportABugExportLogs".localized(), + body: .text("helpReportABugExportLogsDescription" + .put(key: "app_name", value: Constants.app_name) + .localized()), + confirmTitle: "save".localized(), + cancelTitle: "share".localized(), cancelStyle: .alert_text, - onConfirm: { _ in UIPasteboard.general.string = latestLogFilePath }, - onCancel: { _ in - HelpViewModel.shareLogsInternal( - viewControllerToDismiss: viewControllerToDismiss, - targetView: targetView, - animated: animated, - onShareComplete: onShareComplete - ) + hasCloseButton: true, + dismissOnConfirm: false, + onConfirm: { modal in + #if targetEnvironment(simulator) + UIPasteboard.general.string = latestLogFilePath + #endif + + modal.dismiss(animated: true) { + HelpViewModel.shareLogsInternal( + viaShareSheet: false, + viewControllerToDismiss: viewControllerToDismiss, + targetView: targetView, + animated: animated, + onShareComplete: onShareComplete + ) + } + }, + onCancel: { modal in + #if targetEnvironment(simulator) + UIPasteboard.general.string = latestLogFilePath + #endif + + modal.dismiss(animated: true) { + HelpViewModel.shareLogsInternal( + viaShareSheet: true, + viewControllerToDismiss: viewControllerToDismiss, + targetView: targetView, + animated: animated, + onShareComplete: onShareComplete + ) + } } ) ) - // stringlint:ignore_stop viewController.present(modal, animated: animated, completion: nil) - #else - HelpViewModel.shareLogsInternal( - viewControllerToDismiss: viewControllerToDismiss, - targetView: targetView, - animated: animated, - onShareComplete: onShareComplete - ) - #endif } private static func shareLogsInternal( + viaShareSheet: Bool, viewControllerToDismiss: UIViewController? = nil, targetView: UIView? = nil, animated: Bool = true, @@ -237,158 +220,70 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa let viewController: UIViewController = Singleton.appContext.frontmostViewController else { return } - let showShareSheet: () -> () = { - let shareVC = UIActivityViewController( - activityItems: [ URL(fileURLWithPath: latestLogFilePath) ], - applicationActivities: nil - ) - shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() } - - if UIDevice.current.isIPad { - shareVC.excludedActivityTypes = [] - shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - shareVC.popoverPresentationController?.sourceView = (targetView ?? viewController.view) - shareVC.popoverPresentationController?.sourceRect = (targetView ?? viewController.view).bounds + let showExportOption: () -> () = { + switch viaShareSheet { + case true: + let shareVC = UIActivityViewController( + activityItems: [ URL(fileURLWithPath: latestLogFilePath) ], + applicationActivities: nil + ) + shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() } + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + shareVC.popoverPresentationController?.sourceView = (targetView ?? viewController.view) + shareVC.popoverPresentationController?.sourceRect = (targetView ?? viewController.view).bounds + } + viewController.present(shareVC, animated: animated, completion: nil) + + case false: + // Create and present the document picker + let documentPickerResult: DocumentPickerResult = DocumentPickerResult { _ in + HelpViewModel.documentPickerResult = nil + onShareComplete?() + } + HelpViewModel.documentPickerResult = documentPickerResult + + let documentPicker: UIDocumentPickerViewController = UIDocumentPickerViewController( + forExporting: [URL(fileURLWithPath: latestLogFilePath)] + ) + documentPicker.delegate = documentPickerResult + documentPicker.modalPresentationStyle = .formSheet + viewController.present(documentPicker, animated: animated, completion: nil) } - viewController.present(shareVC, animated: animated, completion: nil) } guard let viewControllerToDismiss: UIViewController = viewControllerToDismiss else { - showShareSheet() + showExportOption() return } viewControllerToDismiss.dismiss(animated: animated) { - showShareSheet() + showExportOption() } } +} + +private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { + private let onResult: (URL?) -> Void + + init(onResult: @escaping (URL?) -> Void) { + self.onResult = onResult + } + + // MARK: - UIDocumentPickerDelegate -#if DEBUG - // stringlint:ignore_contents - private func exportDatabase(_ targetView: UIView?) { - let generatedPassword: String = UUID().uuidString - self.databaseKeyEncryptionPassword = generatedPassword + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url: URL = urls.first else { + self.onResult(nil) + return + } - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Export Database", - body: .input( - explanation: NSAttributedString( - string: """ - Sharing the database and key together is dangerous! - - We've generated a secure password for you but feel free to provide your own (we will show the generated password again after exporting) - - This password will be used to encrypt the database decryption key and will be exported alongside the database - """ - ), - placeholder: "Enter a password", - initialValue: generatedPassword, - clearButton: true, - onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } - ), - confirmTitle: "Export", - dismissOnConfirm: false, - onConfirm: { [weak self] modal in - modal.dismiss(animated: true) { - guard let password: String = self?.databaseKeyEncryptionPassword, password.count >= 6 else { - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Error", - body: .text("Password must be at least 6 characters") - ) - ), - transitionType: .present - ) - return - } - - do { - let exportInfo = try Storage.shared.exportInfo(password: password) - let shareVC = UIActivityViewController( - activityItems: [ - URL(fileURLWithPath: exportInfo.dbPath), - URL(fileURLWithPath: exportInfo.keyPath) - ], - applicationActivities: nil - ) - shareVC.completionWithItemsHandler = { [weak self] _, completed, _, _ in - guard - completed && - generatedPassword == self?.databaseKeyEncryptionPassword - else { return } - - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Password", - body: .text(""" - The generated password was: - \(generatedPassword) - - Avoid sending this via the same means as the database - """), - confirmTitle: "Share", - dismissOnConfirm: false, - onConfirm: { [weak self] modal in - modal.dismiss(animated: true) { - let passwordShareVC = UIActivityViewController( - activityItems: [generatedPassword], - applicationActivities: nil - ) - if UIDevice.current.isIPad { - passwordShareVC.excludedActivityTypes = [] - passwordShareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - passwordShareVC.popoverPresentationController?.sourceView = targetView - passwordShareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) - } - - self?.transitionToScreen(passwordShareVC, transitionType: .present) - } - } - ) - ), - transitionType: .present - ) - } - - if UIDevice.current.isIPad { - shareVC.excludedActivityTypes = [] - shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - shareVC.popoverPresentationController?.sourceView = targetView - shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) - } - - self?.transitionToScreen(shareVC, transitionType: .present) - } - catch { - let message: String = { - switch error { - case CryptoKitError.incorrectKeySize: - return "The password must be between 6 and 32 characters (padded to 32 bytes)" - - default: return "Failed to export database" - } - }() - - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Error", - body: .text(message) - ) - ), - transitionType: .present - ) - } - } - } - ) - ), - transitionType: .present - ) + self.onResult(url) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + self.onResult(nil) } -#endif } diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 003900c7ed0..0fb869d46dc 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -90,6 +90,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl case inviteAFriend case recoveryPhrase case help + case developerSettings case clearData } @@ -210,8 +211,10 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .eraseToAnyPublisher() // MARK: - Content + private struct State: Equatable { let profile: Profile + let developerModeEnabled: Bool let hideRecoveryPasswordPermanently: Bool } @@ -221,217 +224,217 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .databaseObservation(self) { [weak self, dependencies] db -> State in State( profile: Profile.fetchOrCreateCurrentUser(db, using: dependencies), + developerModeEnabled: db[.developerModeEnabled], hideRecoveryPasswordPermanently: db[.hideRecoveryPasswordPermanently] ) } - .map { [weak self] state -> [SectionModel] in - return [ - SectionModel( - model: .profileInfo, - elements: [ - SessionCell.Info( - id: .avatar, - accessory: .profile( - id: state.profile.id, - size: .hero, - profile: state.profile - ), - styling: SessionCell.StyleInfo( - alignment: .centerHugging, - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), - backgroundStyle: .noBackground - ), + .map { [weak self, dependencies] state -> [SectionModel] in + let profileInfo: SectionModel = SectionModel( + model: .profileInfo, + elements: [ + SessionCell.Info( + id: .avatar, + accessory: .profile( + id: state.profile.id, + size: .hero, + profile: state.profile + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "User settings", + label: "Profile picture" + ), + onTap: { + self?.updateProfilePicture(currentFileName: state.profile.profilePictureFileName) + } + ), + SessionCell.Info( + id: .profileName, + title: SessionCell.TextInfo( + state.profile.displayName(), + font: .titleLarge, + alignment: .center, + interaction: .editable + ), + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding(top: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Username", + label: state.profile.displayName() + ), + onTap: { self?.setIsEditing(true) } + ) + ] + ) + let sessionId: SectionModel = SectionModel( + model: .sessionId, + elements: [ + SessionCell.Info( + id: .sessionId, + title: SessionCell.TextInfo( + state.profile.id, + font: .monoLarge, + alignment: .center, + interaction: .copy + ), + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Account ID", + label: state.profile.id + ) + ), + SessionCell.Info( + id: .idActions, + leftAccessory: .button( + style: .bordered, + title: "share".localized(), accessibility: Accessibility( - identifier: "User settings", - label: "Profile picture" + identifier: "Share button", + label: "Share button" ), - onTap: { - self?.updateProfilePicture(currentFileName: state.profile.profilePictureFileName) + run: { _ in + self?.shareSessionId(state.profile.id) } ), - SessionCell.Info( - id: .profileName, - title: SessionCell.TextInfo( - state.profile.displayName(), - font: .titleLarge, - alignment: .center, - interaction: .editable - ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding(top: Values.smallSpacing), - backgroundStyle: .noBackground - ), + rightAccessory: .button( + style: .bordered, + title: "copy".localized(), accessibility: Accessibility( - identifier: "Username", - label: state.profile.displayName() - ), - onTap: { self?.setIsEditing(true) } - ) - ] - ), - SectionModel( - model: .sessionId, - elements: [ - SessionCell.Info( - id: .sessionId, - title: SessionCell.TextInfo( - state.profile.id, - font: .monoLarge, - alignment: .center, - interaction: .copy + identifier: "Copy button", + label: "Copy button" ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), - backgroundStyle: .noBackground - ), - accessibility: Accessibility( - identifier: "Account ID", - label: state.profile.id - ) + run: { button in + self?.copySessionId(state.profile.id, button: button) + } ), - SessionCell.Info( - id: .idActions, - leftAccessory: .button( - style: .bordered, - title: "share".localized(), - accessibility: Accessibility( - identifier: "Share button", - label: "Share button" - ), - run: { _ in - self?.shareSessionId(state.profile.id) - } - ), - rightAccessory: .button( - style: .bordered, - title: "copy".localized(), - accessibility: Accessibility( - identifier: "Copy button", - label: "Copy button" - ), - run: { button in - self?.copySessionId(state.profile.id, button: button) - } + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + leading: 0, + trailing: 0 ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding( - top: Values.smallSpacing, - leading: 0, - trailing: 0 - ), - backgroundStyle: .noBackground - ) + backgroundStyle: .noBackground ) - ] - ), - SectionModel( - model: .menus, - elements: [ - SessionCell.Info( - id: .path, - leftAccessory: .customView(hashValue: "PathStatusView") { // stringlint:ignore - // Need to ensure this view is the same size as the icons so - // wrap it in a larger view - let result: UIView = UIView() - let pathView: PathStatusView = PathStatusView(size: .large) - result.addSubview(pathView) - - result.set(.width, to: IconSize.medium.size) - result.set(.height, to: IconSize.medium.size) - pathView.center(in: result) - - return result - }, - title: "onionRoutingPath".localized(), - onTap: { self?.transitionToScreen(PathVC()) } + ) + ] + ) + let menus: SectionModel = SectionModel( + model: .menus, + elements: [ + SessionCell.Info( + id: .path, + leftAccessory: .customView(hashValue: "PathStatusView") { // stringlint:ignore + // Need to ensure this view is the same size as the icons so + // wrap it in a larger view + let result: UIView = UIView() + let pathView: PathStatusView = PathStatusView(size: .large) + result.addSubview(pathView) + + result.set(.width, to: IconSize.medium.size) + result.set(.height, to: IconSize.medium.size) + pathView.center(in: result) + + return result + }, + title: "onionRoutingPath".localized(), + onTap: { self?.transitionToScreen(PathVC()) } + ), + SessionCell.Info( + id: .privacy, + leftAccessory: .icon( + UIImage(named: "icon_privacy")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .privacy, - leftAccessory: .icon( - UIImage(named: "icon_privacy")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionPrivacy".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: PrivacySettingsViewModel()) - ) - } + title: "sessionPrivacy".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: PrivacySettingsViewModel()) + ) + } + ), + SessionCell.Info( + id: .notifications, + leftAccessory: .icon( + UIImage(named: "icon_speaker")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .notifications, - leftAccessory: .icon( - UIImage(named: "icon_speaker")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionNotifications".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: NotificationSettingsViewModel()) - ) - } + title: "sessionNotifications".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: NotificationSettingsViewModel()) + ) + } + ), + SessionCell.Info( + id: .conversations, + leftAccessory: .icon( + UIImage(named: "icon_msg")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .conversations, - leftAccessory: .icon( - UIImage(named: "icon_msg")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionConversations".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: ConversationSettingsViewModel()) - ) - } + title: "sessionConversations".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: ConversationSettingsViewModel()) + ) + } + ), + SessionCell.Info( + id: .messageRequests, + leftAccessory: .icon( + UIImage(named: "icon_msg_req")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .messageRequests, - leftAccessory: .icon( - UIImage(named: "icon_msg_req")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionMessageRequests".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: MessageRequestsViewModel()) - ) - } + title: "sessionMessageRequests".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: MessageRequestsViewModel()) + ) + } + ), + SessionCell.Info( + id: .appearance, + leftAccessory: .icon( + UIImage(named: "icon_apperance")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .appearance, - leftAccessory: .icon( - UIImage(named: "icon_apperance")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionAppearance".localized(), - onTap: { - self?.transitionToScreen(AppearanceViewController()) - } + title: "sessionAppearance".localized(), + onTap: { + self?.transitionToScreen(AppearanceViewController()) + } + ), + SessionCell.Info( + id: .inviteAFriend, + leftAccessory: .icon( + UIImage(named: "icon_invite")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .inviteAFriend, - leftAccessory: .icon( - UIImage(named: "icon_invite")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionInviteAFriend".localized(), - onTap: { - let invitation: String = "accountIdShare" - .put(key: "app_name", value: Constants.app_name) - .put(key: "account_id", value: state.profile.id) - .put(key: "session_download_url", value: Constants.session_download_url) - .localized() - - self?.transitionToScreen( - UIActivityViewController( - activityItems: [ invitation ], - applicationActivities: nil - ), - transitionType: .present - ) - } - ) - ].appending( + title: "sessionInviteAFriend".localized(), + onTap: { + let invitation: String = "accountIdShare" + .put(key: "app_name", value: Constants.app_name) + .put(key: "account_id", value: state.profile.id) + .put(key: "session_download_url", value: Constants.session_download_url) + .localized() + + self?.transitionToScreen( + UIActivityViewController( + activityItems: [ invitation ], + applicationActivities: nil + ), + transitionType: .present + ) + } + ), + ( state.hideRecoveryPasswordPermanently ? nil : SessionCell.Info( id: .recoveryPhrase, @@ -462,43 +465,66 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } } ) - ).appending(contentsOf: [ + ), + SessionCell.Info( + id: .help, + leftAccessory: .icon( + UIImage(named: "icon_help")? + .withRenderingMode(.alwaysTemplate) + ), + title: "sessionHelp".localized(), + onTap: { + self?.transitionToScreen( + SessionTableViewController(viewModel: HelpViewModel()) + ) + } + ), + (!state.developerModeEnabled ? nil : SessionCell.Info( - id: .help, + id: .developerSettings, leftAccessory: .icon( - UIImage(named: "icon_help")? + UIImage(systemName: "wrench.and.screwdriver")? .withRenderingMode(.alwaysTemplate) ), - title: "sessionHelp".localized(), + title: "Developer Settings", // stringlint:ignore + styling: SessionCell.StyleInfo(tintColor: .warning), onTap: { self?.transitionToScreen( - SessionTableViewController(viewModel: HelpViewModel()) + SessionTableViewController(viewModel: DeveloperSettingsViewModel(using: dependencies)) ) } - ), - SessionCell.Info( - id: .clearData, - leftAccessory: .icon( - UIImage(named: "icon_bin")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionClearData".localized(), - styling: SessionCell.StyleInfo(tintColor: .danger), - onTap: { - self?.transitionToScreen(NukeDataModal(), transitionType: .present) - } ) - ]) - ) - ] + ), + SessionCell.Info( + id: .clearData, + leftAccessory: .icon( + UIImage(named: "icon_bin")? + .withRenderingMode(.alwaysTemplate) + ), + title: "sessionClearData".localized(), + styling: SessionCell.StyleInfo(tintColor: .danger), + onTap: { + self?.transitionToScreen(NukeDataModal(), transitionType: .present) + } + ) + ].compactMap { $0 } + ) + + return [profileInfo, sessionId, menus] } - public let footerView: AnyPublisher = Just(VersionFooterView()).eraseToAnyPublisher() + public lazy var footerView: AnyPublisher = Just(VersionFooterView(numTaps: 9) { [dependencies] in + /// Do nothing if developer mode is already enabled + guard !dependencies.storage[.developerModeEnabled] else { return } + + dependencies.storage.write { db in + db[.developerModeEnabled] = true + } + }).eraseToAnyPublisher() // MARK: - Functions private func updateProfilePicture(currentFileName: String?) { - let existingDisplayName: String = self.oldDisplayName let existingImageData: Data? = ProfileManager .profileAvatar(id: self.userSessionId) let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info( diff --git a/Session/Settings/Views/VersionFooterView.swift b/Session/Settings/Views/VersionFooterView.swift index fa0eb801ce1..539f94d9cf1 100644 --- a/Session/Settings/Views/VersionFooterView.swift +++ b/Session/Settings/Views/VersionFooterView.swift @@ -7,6 +7,8 @@ class VersionFooterView: UIView { private static let footerHeight: CGFloat = 75 private static let logoHeight: CGFloat = 24 + private let multiTapCallback: (() -> Void)? + // MARK: - UI private lazy var logoImageView: UIImageView = { @@ -38,23 +40,25 @@ class VersionFooterView: UIView { let version: String = ((infoDict?["CFBundleShortVersionString"] as? String) ?? "0.0.0") let buildNumber: String? = (infoDict?["CFBundleVersion"] as? String) let commitInfo: String? = (infoDict?["GitCommitHash"] as? String) - let buildInfo: String = [buildNumber, commitInfo] + let buildInfo: String? = [buildNumber, commitInfo] .compactMap { $0 } .joined(separator: " - ") + .nullIfEmpty + .map { "(\($0))" } // stringlint:ignore_stop result.text = [ "Version \(version)", - (!buildInfo.isEmpty ? " (" : ""), - buildInfo, - (!buildInfo.isEmpty ? ")" : ""), - ].joined() + buildInfo + ].compactMap { $0 }.joined(separator: " ") return result }() // MARK: - Initialization - init() { + init(numTaps: Int = 0, multiTapCallback: (() -> Void)? = nil) { + self.multiTapCallback = multiTapCallback + // Note: Need to explicitly set the height for a table footer view // or it will have no height super.init( @@ -66,7 +70,7 @@ class VersionFooterView: UIView { ) ) - setupViewHierarchy() + setupViewHierarchy(numTaps: numTaps) } required init?(coder: NSCoder) { @@ -75,7 +79,7 @@ class VersionFooterView: UIView { // MARK: - Content - private func setupViewHierarchy() { + private func setupViewHierarchy(numTaps: Int) { addSubview(logoImageView) addSubview(versionLabel) @@ -84,5 +88,18 @@ class VersionFooterView: UIView { versionLabel.pin(.top, to: .bottom, of: logoImageView, withInset: Values.mediumSpacing) versionLabel.pin(.left, to: .left, of: self) versionLabel.pin(.right, to: .right, of: self) + + if numTaps > 0 { + let tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(onMultiTap) + ) + tapGestureRecognizer.numberOfTapsRequired = numTaps + addGestureRecognizer(tapGestureRecognizer) + } + } + + @objc private func onMultiTap() { + self.multiTapCallback?() } } diff --git a/Session/Shared/Types/ObservableTableSource.swift b/Session/Shared/Types/ObservableTableSource.swift index 006a9dd5d7e..5283e5b78ba 100644 --- a/Session/Shared/Types/ObservableTableSource.swift +++ b/Session/Shared/Types/ObservableTableSource.swift @@ -22,6 +22,11 @@ public protocol ObservableTableSource: AnyObject, SectionedTableData { func didReturnFromBackground() } +public enum ObservableTableSourceRefreshType { + case databaseQuery + case postDatabaseQuery +} + extension ObservableTableSource { public var pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never> { self.observableState.pendingTableDataSubject @@ -33,25 +38,33 @@ extension ObservableTableSource { public var tableDataPublisher: TargetPublisher { self.observation.finalPublisher(self, using: dependencies) } public func didReturnFromBackground() {} - public func forceRefresh() { self.observableState._forcedRefresh.send(()) } + public func forceRefresh(type: ObservableTableSourceRefreshType = .databaseQuery) { + switch type { + case .databaseQuery: self.observableState._forcedRequery.send(()) + case .postDatabaseQuery: self.observableState._forcedPostQueryRefresh.send(()) + } + } } // MARK: - State Manager (ObservableTableSource) public class ObservableTableSourceState: SectionedTableData { - public let forcedRefresh: AnyPublisher + fileprivate let forcedRequery: AnyPublisher + fileprivate let forcedPostQueryRefresh: AnyPublisher public let pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never> // MARK: - Internal Variables fileprivate var hasEmittedInitialData: Bool - fileprivate let _forcedRefresh: PassthroughSubject = PassthroughSubject() + fileprivate let _forcedRequery: PassthroughSubject = PassthroughSubject() + fileprivate let _forcedPostQueryRefresh: PassthroughSubject = PassthroughSubject() // MARK: - Initialization init() { self.hasEmittedInitialData = false - self.forcedRefresh = _forcedRefresh.shareReplay(0) + self.forcedRequery = _forcedRequery.shareReplay(0) + self.forcedPostQueryRefresh = _forcedPostQueryRefresh.shareReplay(0) self.pendingTableDataSubject = CurrentValueSubject([]) } } @@ -152,29 +165,67 @@ public enum ObservationBuilder { /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this return TableObservation { viewModel, dependencies in - return ValueObservation - .trackingConstantRegion(fetch) - .removeDuplicates() - .handleEvents(didFail: { SNLog("[\(type(of: viewModel))] Observation failed with error: \($0)") }) - .publisher(in: dependencies.storage, scheduling: dependencies.scheduler) - .manualRefreshFrom(source.observableState.forcedRefresh) + let subject: CurrentValueSubject = CurrentValueSubject(nil) + var forcedRefreshCancellable: AnyCancellable? + var observationCancellable: DatabaseCancellable? + + /// In order to force a `ValueObservation` to requery we need to resubscribe to it, as a result we create a + /// `CurrentValueSubject` and in the `receiveSubscription` call we start the `ValueObservation` sending + /// it's output into the subject + /// + /// **Note:** We need to use a `CurrentValueSubject` here because the `ValueObservation` could send it's + /// first value _before_ the subscription is properly setup, by using a `CurrentValueSubject` the value will be stored + /// and emitted once the subscription becomes valid + return subject + .compactMap { $0 } + .handleEvents( + receiveSubscription: { subscription in + forcedRefreshCancellable = source.observableState.forcedRequery + .prepend(()) + .sink( + receiveCompletion: { _ in }, + receiveValue: { _ in + /// Cancel any previous observation and create a brand new observation for this refresh + /// + /// **Note:** The `ValueObservation` **MUST** be started from the main thread + observationCancellable?.cancel() + observationCancellable = dependencies.storage.start( + ValueObservation + .trackingConstantRegion(fetch) + .removeDuplicates(), + scheduling: dependencies.scheduler, + onError: { error in + let log: String = [ + "[\(type(of: viewModel))]", // stringlint:ignore + "Observation failed with error:", // stringlint:ignore + "\(error)" // stringlint:ignore + ].joined(separator: " ") + SNLog(log) + subject.send(completion: Subscribers.Completion.failure(error)) + }, + onChange: { subject.send($0) } + ) + } + ) + }, + receiveCancel: { + forcedRefreshCancellable?.cancel() + observationCancellable?.cancel() + } + ) + .manualRefreshFrom(source.observableState.forcedPostQueryRefresh) + .shareReplay(1) // Share to prevent multiple subscribers resulting in multiple ValueObservations + .eraseToAnyPublisher() } } - /// The `ValueObserveration` will trigger whenever any of the data fetched in the closure is updated, please see the following link for tips - /// to help optimise performance https://github.com/groue/GRDB.swift#valueobservation-performance - static func databaseObservation(_ source: S, fetch: @escaping (Database) throws -> [T]) -> TableObservation<[T]> { - /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) - /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own - /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) - /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this + static func refreshableData(_ source: S, fetch: @escaping () -> T) -> TableObservation { return TableObservation { viewModel, dependencies in - return ValueObservation - .trackingConstantRegion(fetch) - .removeDuplicates() - .handleEvents(didFail: { SNLog("[\(type(of: viewModel))] Observation failed with error: \($0)") }) - .publisher(in: dependencies.storage, scheduling: dependencies.scheduler) - .manualRefreshFrom(source.observableState.forcedRefresh) + source.observableState.forcedRequery + .prepend(()) + .setFailureType(to: Error.self) + .map { _ in fetch() } + .manualRefreshFrom(source.observableState.forcedPostQueryRefresh) } } } diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 7c1548f4187..6e426327435 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -593,7 +593,7 @@ extension Attachment { return UInt(floor(max(screenSizePoints.width, screenSizePoints.height) * minZoomFactor)) }() - private static var sharedDataAttachmentsDirPath: String = { + public static var sharedDataAttachmentsDirPath: String = { URL(fileURLWithPath: FileManager.default.appSharedDataDirectoryPath) .appendingPathComponent("Attachments") // stringlint:ignore .path diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index c1b15ce893e..3331481c2e3 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -73,6 +73,10 @@ public extension Setting.BoolKey { /// Controls whether the device will poll for community message requests (SOGS `/inbox` endpoint) static let checkForCommunityMessageRequests: Setting.BoolKey = "checkForCommunityMessageRequests" + + /// Controls whether developer mode is enabled (this displays a section within the Settings screen which allows manual control of feature flags + /// and system settings for better debugging) + static let developerModeEnabled: Setting.BoolKey = "developerModeEnabled" } // stringlint:ignore_contents diff --git a/SessionUIKit/Components/ConfirmationModal.swift b/SessionUIKit/Components/ConfirmationModal.swift index 971c54368d2..a8256496de7 100644 --- a/SessionUIKit/Components/ConfirmationModal.swift +++ b/SessionUIKit/Components/ConfirmationModal.swift @@ -167,6 +167,10 @@ public class ConfirmationModal: Modal, UITextFieldDelegate { fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + public override func populateContentView() { let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( target: self, @@ -188,6 +192,24 @@ public class ConfirmationModal: Modal, UITextFieldDelegate { mainStackView.pin(to: contentView) closeButton.pin(.top, to: .top, of: contentView, withInset: 8) closeButton.pin(.right, to: .right, of: contentView, withInset: -8) + + // Observe keyboard notifications + let keyboardNotifications: [Notification.Name] = [ + UIResponder.keyboardWillShowNotification, + UIResponder.keyboardDidShowNotification, + UIResponder.keyboardWillChangeFrameNotification, + UIResponder.keyboardDidChangeFrameNotification, + UIResponder.keyboardWillHideNotification, + UIResponder.keyboardDidHideNotification + ] + keyboardNotifications.forEach { notification in + NotificationCenter.default.addObserver( + self, + selector: #selector(handleKeyboardNotification(_:)), + name: notification, + object: nil + ) + } } // MARK: - Content @@ -325,6 +347,67 @@ public class ConfirmationModal: Modal, UITextFieldDelegate { override public func cancel() { internalOnCancel?(self) } + + // MARK: - Keyboard Avoidance + + @objc func handleKeyboardNotification(_ notification: Notification) { + guard + let userInfo: [AnyHashable: Any] = notification.userInfo, + var keyboardEndFrame: CGRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect + else { return } + + // If reduce motion+crossfade transitions is on, in iOS 14 UIKit vends out a keyboard end frame + // of CGRect zero. This breaks the math below. + // + // If our keyboard end frame is CGRectZero, build a fake rect that's translated off the bottom edge. + if keyboardEndFrame == .zero { + keyboardEndFrame = CGRect( + x: UIScreen.main.bounds.minX, + y: UIScreen.main.bounds.maxY, + width: UIScreen.main.bounds.width, + height: 0 + ) + } + + // No nothing if there was no change +// let keyboardEndFrameConverted: CGRect = self.view.convert(keyboardEndFrame, from: nil) +// guard keyboardEndFrameConverted != lastKnownKeyboardFrame else { return } +// +// self.lastKnownKeyboardFrame = keyboardEndFrameConverted + + // Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600 + // and https://stackoverflow.com/a/25260930 to better understand what we are + // doing with the UIViewAnimationOptions + let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue)) + let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16)) + let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0) + + guard duration > 0, !UIAccessibility.isReduceMotionEnabled else { + // UIKit by default (sometimes? never?) animates all changes in response to keyboard events. + // We want to suppress those animations if the view isn't visible, + // otherwise presentation animations don't work properly. + UIView.performWithoutAnimation { + self.updateKeyboardAvoidance(keyboardEndFrame: keyboardEndFrame) + } + return + } + + UIView.animate( + withDuration: duration, + delay: 0, + options: options, + animations: { [weak self] in + self?.updateKeyboardAvoidance(keyboardEndFrame: keyboardEndFrame) + self?.view.layoutIfNeeded() + }, + completion: nil + ) + } + + private func updateKeyboardAvoidance(keyboardEndFrame: CGRect) { + contentTopConstraint?.isActive = (keyboardEndFrame.minY < (view.bounds.height - 100)) + contentCenterYConstraint?.isActive = (contentTopConstraint?.isActive != true) + } } // MARK: - Types diff --git a/SessionUIKit/Components/Modal.swift b/SessionUIKit/Components/Modal.swift index 9d52c574886..52f19d44a27 100644 --- a/SessionUIKit/Components/Modal.swift +++ b/SessionUIKit/Components/Modal.swift @@ -16,6 +16,9 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { // MARK: - Components + internal var contentTopConstraint: NSLayoutConstraint? + internal var contentCenterYConstraint: NSLayoutConstraint? + private lazy var dimmingView: UIView = { let result = UIVisualEffectView() @@ -97,7 +100,6 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { if UIDevice.current.isIPad { containerView.set(.width, to: Values.iPadModalWidth) - containerView.center(in: view) } else { containerView.leadingAnchor @@ -106,9 +108,14 @@ open class Modal: UIViewController, UIGestureRecognizerDelegate { view.trailingAnchor .constraint(equalTo: containerView.trailingAnchor, constant: Values.veryLargeSpacing) .isActive = true - containerView.center(.vertical, in: view) } + containerView.center(.horizontal, in: view) + contentCenterYConstraint = containerView.center(.vertical, in: view) + contentTopConstraint = containerView + .pin(.top, toMargin: .top, of: view) + .setting(isActive: false) + // Gestures let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close)) swipeGestureRecognizer.direction = .down diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 82352e7be57..57013e1b324 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -732,9 +732,8 @@ public extension ValueObservation { // MARK: - Debug Convenience -#if DEBUG public extension Storage { - func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) { + func secureExportKey(password: String) throws -> String { var keySpec: Data = try getOrGenerateDatabaseKeySpec() defer { keySpec.resetBytes(in: 0.. Void)? + ) throws { + guard FileManager.default.fileExists(atPath: sourcePath) else { + throw ArchiveError.invalidSourcePath + } + + let sourceUrl: URL = URL(fileURLWithPath: sourcePath) + let destinationUrl: URL = URL(fileURLWithPath: destinationPath) + + // Create output stream for backup and compression + guard let outputStream: OutputStream = OutputStream(url: destinationUrl, append: false) else { + throw ArchiveError.archiveFailed + } + + outputStream.open() + defer { outputStream.close() } + + // Stream-based directory traversal and compression + let enumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( + at: sourceUrl, + includingPropertiesForKeys: [.isRegularFileKey] + ) + let fileUrls: [URL] = (enumerator?.allObjects.compactMap { $0 as? URL } ?? []) + .appending(contentsOf: additionalPaths.map { URL(fileURLWithPath: $0) }) + var index: Int = 0 + progressChanged?(index, fileUrls.count, 0, 0) + + try fileUrls.forEach { url in + index += 1 + + try exportFile( + sourcePath: sourcePath, + fileURL: url, + outputStream: outputStream, + password: password, + index: index, + totalFiles: (fileUrls.count + additionalPaths.count), + isExtraFile: false, + progressChanged: progressChanged + ) + } + + // Add any extra files which we want to include + try additionalPaths.forEach { path in + index += 1 + // TODO: Need to fix these so that the path replacement with `sourcePath` isn't busted + try exportFile( + sourcePath: sourcePath, + fileURL: URL(fileURLWithPath: path), + outputStream: outputStream, + password: password, + index: index, + totalFiles: (fileUrls.count + additionalPaths.count), + isExtraFile: true, + progressChanged: progressChanged + ) + } + } + + private static func exportFile( + sourcePath: String, + fileURL: URL, + outputStream: OutputStream, + password: String?, + index: Int, + totalFiles: Int, + isExtraFile: Bool, + progressChanged: ((Int, Int, UInt64, UInt64) -> Void)? + ) throws { + guard + let values: URLResourceValues = try? fileURL.resourceValues( + forKeys: [.isRegularFileKey, .fileSizeKey] + ), + values.isRegularFile == true, + var fileSize: UInt64 = values.fileSize.map({ UInt64($0) }) + else { + progressChanged?(index, totalFiles, 1, 1) + return + } + + // Relative path preservation + let relativePath: String = fileURL.path.replacingOccurrences( + of: sourcePath, + with: "" + ).trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + // Write path length and path + let pathData: Data = relativePath.data(using: .utf8)! + var pathLength: UInt32 = UInt32(pathData.count) + var isExtraFile: Bool = isExtraFile + + // Encrypt and write metadata (path length + path data) + let metadata: Data = ( + Data(bytes: &pathLength, count: MemoryLayout.size) + + pathData + + Data(bytes: &fileSize, count: MemoryLayout.size) + + Data(bytes: &isExtraFile, count: MemoryLayout.size) + ) + let processedMetadata: [UInt8] + + switch password { + case .none: processedMetadata = Array(metadata) + case .some(let password): + processedMetadata = try encrypt( + buffer: Array(metadata), + password: password + ) + } + + var blockSize: UInt64 = UInt64(processedMetadata.count) + let blockSizeData: [UInt8] = Array(Data(bytes: &blockSize, count: MemoryLayout.size)) + outputStream.write(blockSizeData, maxLength: blockSizeData.count) + outputStream.write(processedMetadata, maxLength: processedMetadata.count) + + // Stream file contents + guard let inputStream: InputStream = InputStream(url: fileURL) else { + progressChanged?(index, totalFiles, 1, 1) + return + } + + inputStream.open() + defer { inputStream.close() } + + var buffer: [UInt8] = [UInt8](repeating: 0, count: 4096) + var currentFileProcessAmount: UInt64 = 0 + while inputStream.hasBytesAvailable { + let bytesRead: Int = inputStream.read(&buffer, maxLength: buffer.count) + currentFileProcessAmount += UInt64(bytesRead) + progressChanged?(index, totalFiles, currentFileProcessAmount, fileSize) + + if bytesRead > 0 { + let processedBytes: [UInt8] + + switch password { + case .none: processedBytes = buffer + case .some(let password): + processedBytes = try encrypt( + buffer: Array(buffer.prefix(bytesRead)), + password: password + ) + } + + var chunkSize: UInt32 = UInt32(processedBytes.count) + let chunkSizeData: [UInt8] = Array(Data(bytes: &chunkSize, count: MemoryLayout.size)) + outputStream.write(chunkSizeData, maxLength: chunkSizeData.count) + outputStream.write(processedBytes, maxLength: processedBytes.count) + } + } + } + + public static func unarchiveDirectory( + archivePath: String, + destinationPath: String, + password: String?, + progressChanged: ((UInt64, UInt64) -> Void)? + ) throws -> [String] { + // Remove any old imported data as we don't want to muddy the new data + if FileManager.default.fileExists(atPath: destinationPath) { + try? FileManager.default.removeItem(atPath: destinationPath) + } + + // Create the destination directory + try FileManager.default.createDirectory( + atPath: destinationPath, + withIntermediateDirectories: true + ) + + guard + let values: URLResourceValues = try? URL(fileURLWithPath: archivePath).resourceValues( + forKeys: [.fileSizeKey] + ), + let encryptedFileSize: UInt64 = values.fileSize.map({ UInt64($0) }), + let inputStream: InputStream = InputStream(fileAtPath: archivePath) + else { throw ArchiveError.unarchiveFailed } + + inputStream.open() + defer { inputStream.close() } + + var extraFilePaths: [String] = [] + var fileAmountProcessed: UInt64 = 0 + progressChanged?(0, encryptedFileSize) + while inputStream.hasBytesAvailable { + // Read block size + var blockSizeBytes: [UInt8] = [UInt8](repeating: 0, count: MemoryLayout.size) + let bytesRead: Int = inputStream.read(&blockSizeBytes, maxLength: blockSizeBytes.count) + fileAmountProcessed += UInt64(bytesRead) + progressChanged?(fileAmountProcessed, encryptedFileSize) + + switch bytesRead { + case 0: continue // We have finished reading + case blockSizeBytes.count: break // We have started the next block + default: throw ArchiveError.unarchiveFailed // Invalid + } + + var blockSize: UInt64 = 0 + _ = withUnsafeMutableBytes(of: &blockSize) { blockSizeBuffer in + blockSizeBytes.copyBytes(to: blockSizeBuffer, from: ...size) + } + + // Read and decrypt metadata + var encryptedMetadata: [UInt8] = [UInt8](repeating: 0, count: Int(blockSize)) + guard inputStream.read(&encryptedMetadata, maxLength: encryptedMetadata.count) == encryptedMetadata.count else { + throw ArchiveError.unarchiveFailed + } + + let metadata: [UInt8] + switch password { + case .none: metadata = encryptedMetadata + case .some(let password): metadata = try decrypt(buffer: encryptedMetadata, password: password) + } + var offset = 0 + + // Extract path length and path + let pathLengthRange: Range = offset..<(offset + MemoryLayout.size) + var pathLength: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &pathLength) { pathLengthBuffer in + metadata.copyBytes(to: pathLengthBuffer, from: pathLengthRange) + } + offset += MemoryLayout.size + + let pathRange: Range = offset..<(offset + Int(pathLength)) + let relativePath: String = String(data: Data(metadata[pathRange]), encoding: .utf8)! + offset += Int(pathLength) + + // Extract file size + let fileSizeRange: Range = offset..<(offset + MemoryLayout.size) + var fileSize: UInt64 = 0 + _ = withUnsafeMutableBytes(of: &fileSize) { fileSizeBuffer in + metadata.copyBytes(to: fileSizeBuffer, from: fileSizeRange) + } + offset += Int(MemoryLayout.size) + + // Extract extra file flag + let isExtraFileRange: Range = offset..<(offset + MemoryLayout.size) + var isExtraFile: Bool = false + _ = withUnsafeMutableBytes(of: &isExtraFile) { isExtraFileBuffer in + metadata.copyBytes(to: isExtraFileBuffer, from: isExtraFileRange) + } + + // Construct full file path + let fullPath: String = (destinationPath as NSString).appendingPathComponent(relativePath) + try FileManager.default.createDirectory( + atPath: (fullPath as NSString).deletingLastPathComponent, + withIntermediateDirectories: true + ) + fileAmountProcessed += UInt64(encryptedMetadata.count) + progressChanged?(fileAmountProcessed, encryptedFileSize) + + // Read and decrypt file content + guard let outputStream: OutputStream = OutputStream(toFileAtPath: fullPath, append: false) else { + throw ArchiveError.unarchiveFailed + } + outputStream.open() + defer { outputStream.close() } + + var remainingFileSize: Int = Int(fileSize) + while remainingFileSize > 0 { + // Read chunk size + var chunkSizeBytes: [UInt8] = [UInt8](repeating: 0, count: MemoryLayout.size) + guard inputStream.read(&chunkSizeBytes, maxLength: chunkSizeBytes.count) == chunkSizeBytes.count else { + throw ArchiveError.unarchiveFailed + } + var chunkSize: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &chunkSize) { chunkSizeBuffer in + chunkSizeBytes.copyBytes(to: chunkSizeBuffer, from: ...size) + } + fileAmountProcessed += UInt64(chunkSizeBytes.count) + progressChanged?(fileAmountProcessed, encryptedFileSize) + + // Read the chunk + var chunkBytes: [UInt8] = [UInt8](repeating: 0, count: Int(chunkSize)) + guard inputStream.read(&chunkBytes, maxLength: chunkBytes.count) == chunkBytes.count else { + throw ArchiveError.unarchiveFailed + } + + let processedChunk: [UInt8] + switch password { + case .none: processedChunk = chunkBytes + case .some(let password): processedChunk = try decrypt(buffer: chunkBytes, password: password) + } + + outputStream.write(processedChunk, maxLength: processedChunk.count) + remainingFileSize -= processedChunk.count + + fileAmountProcessed += UInt64(chunkBytes.count) + progressChanged?(fileAmountProcessed, encryptedFileSize) + } + + if isExtraFile { + extraFilePaths.append(fullPath) + } + } + + return extraFilePaths + } + + private static func encrypt(buffer: [UInt8], password: String) throws -> [UInt8] { + guard let passwordData: Data = password.data(using: .utf8) else { + return buffer + } + + // Use HKDF for key derivation + let salt: Data = Data(count: 16) + let key: SymmetricKey = SymmetricKey(data: passwordData) + let symmetricKey: SymmetricKey = SymmetricKey( + data: HKDF.deriveKey( + inputKeyMaterial: key, + salt: salt, + outputByteCount: 32 + ) + ) + let nonce: AES.GCM.Nonce = AES.GCM.Nonce() + let sealedBox: AES.GCM.SealedBox = try AES.GCM.seal( + Data(buffer), + using: symmetricKey, + nonce: nonce + ) + + // Combine nonce, ciphertext, and tag + return [UInt8](nonce) + sealedBox.ciphertext + sealedBox.tag + } + + private static func decrypt(buffer: [UInt8], password: String) throws -> [UInt8] { + guard let passwordData: Data = password.data(using: .utf8) else { + return buffer + } + + let salt: Data = Data(count: 16) + let key: SymmetricKey = SymmetricKey(data: passwordData) + let symmetricKey: SymmetricKey = SymmetricKey( + data: HKDF.deriveKey( + inputKeyMaterial: key, + salt: salt, + outputByteCount: 32 + ) + ) + + // Extract nonce, ciphertext, and tag + let nonce: AES.GCM.Nonce = try AES.GCM.Nonce(data: Data(buffer.prefix(12))) + let ciphertext: Data = Data(buffer[12..<(buffer.count-16)]) + let tag: Data = Data(buffer.suffix(16)) + + // Decrypt with AES-GCM + let sealedBox: AES.GCM.SealedBox = try AES.GCM.SealedBox( + nonce: nonce, + ciphertext: ciphertext, + tag: tag + ) + + let decryptedData: Data = try AES.GCM.open(sealedBox, using: symmetricKey) + return [UInt8](decryptedData) + } +} + +fileprivate extension InputStream { + func readEncryptedChunk(password: String, maxLength: Int) -> Data? { + var buffer: [UInt8] = [UInt8](repeating: 0, count: maxLength) + let bytesRead: Int = self.read(&buffer, maxLength: maxLength) + guard bytesRead > 0 else { return nil } + + return Data(buffer.prefix(bytesRead)) + } +} diff --git a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift index fe23a97d12d..54ba079c0df 100644 --- a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift +++ b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift @@ -31,6 +31,28 @@ public class ModalActivityIndicatorViewController: OWSViewController { return result }() + private lazy var stackView: UIStackView = { + let result: UIStackView = UIStackView() + result.axis = .vertical + result.spacing = Values.largeSpacing + result.alignment = .center + + return result + }() + + private lazy var messageLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.text = message + result.textAlignment = .center + result.themeTextColor = .textPrimary + result.lineBreakMode = .byWordWrapping + result.numberOfLines = 0 + result.isHidden = (message == nil) + + return result + }() + private lazy var spinner: NVActivityIndicatorView = { let result: NVActivityIndicatorView = NVActivityIndicatorView( frame: CGRect.zero, @@ -120,31 +142,15 @@ public class ModalActivityIndicatorViewController: OWSViewController { self.view.themeBackgroundColor = .clear self.view.addSubview(dimmingView) + self.view.addSubview(stackView) dimmingView.pin(to: self.view) + stackView.center(in: self.view) + + stackView.addArrangedSubview(spinner) + stackView.addArrangedSubview(messageLabel) + + messageLabel.set(.width, to: .width, of: stackView, withOffset: -(2 * Values.mediumSpacing)) - if let message = message { - let messageLabel: UILabel = UILabel() - messageLabel.font = .systemFont(ofSize: Values.mediumFontSize) - messageLabel.text = message - messageLabel.themeTextColor = .textPrimary - messageLabel.numberOfLines = 0 - messageLabel.textAlignment = .center - messageLabel.lineBreakMode = .byWordWrapping - messageLabel.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing) - - let stackView = UIStackView(arrangedSubviews: [ messageLabel, spinner ]) - stackView.axis = .vertical - stackView.spacing = Values.largeSpacing - stackView.alignment = .center - self.view.addSubview(stackView) - - stackView.center(in: self.view) - } - else { - self.view.addSubview(spinner) - spinner.center(in: self.view) - } - if canCancel { let cancelButton: SessionButton = SessionButton(style: .destructive, size: .large) cancelButton.setTitle("cancel".localized(), for: .normal) @@ -190,4 +196,9 @@ public class ModalActivityIndicatorViewController: OWSViewController { dismiss { } } + + public func setMessage(_ message: String?) { + messageLabel.text = message + messageLabel.isHidden = (message == nil) + } } From f153fb6cf5353235ebb991f6d3781bbb62be02e0 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 4 Dec 2024 15:14:05 +1100 Subject: [PATCH 20/33] add logic to prevent duplicated message request response being inserted into database --- .../MessageReceiver+MessageRequests.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index c9d240fbfa6..20819912fef 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -117,7 +117,7 @@ extension MessageReceiver { } // Update the `didApproveMe` state of the sender - try updateContactApprovalStatusIfNeeded( + let shouldInsertControlMessage: Bool = try updateContactApprovalStatusIfNeeded( db, senderSessionId: senderId, threadId: nil @@ -138,6 +138,7 @@ extension MessageReceiver { ) } + guard shouldInsertControlMessage else { return } // Notify the user of their approval (Note: This will always appear in the un-blinded thread) // // Note: We want to do this last as it'll mean the un-blinded thread gets updated and the @@ -156,11 +157,11 @@ extension MessageReceiver { ).inserted(db) } - internal static func updateContactApprovalStatusIfNeeded( + @discardableResult internal static func updateContactApprovalStatusIfNeeded( _ db: Database, senderSessionId: String, threadId: String? - ) throws { + ) throws -> Bool { let userPublicKey: String = getUserHexEncodedPublicKey(db) // If the sender of the message was the current user @@ -171,13 +172,13 @@ extension MessageReceiver { let threadId: String = threadId, let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId), !thread.isNoteToSelf(db) - else { return } + else { return true } // Sending a message to someone flags them as approved so create the contact record if // it doesn't exist let contact: Contact = Contact.fetchOrCreate(db, id: threadId) - guard !contact.isApproved else { return } + guard !contact.isApproved else { return false } try? contact.save(db) _ = try? Contact @@ -189,12 +190,14 @@ extension MessageReceiver { // someone without approving them) let contact: Contact = Contact.fetchOrCreate(db, id: senderSessionId) - guard !contact.didApproveMe else { return } + guard !contact.didApproveMe else { return false } try? contact.save(db) _ = try? Contact .filter(id: senderSessionId) .updateAllAndConfig(db, Contact.Columns.didApproveMe.set(to: true)) } + + return true } } From 25ffe98912dd814097b560ba99510e0980d25dc2 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 4 Dec 2024 16:50:13 +1100 Subject: [PATCH 21/33] fix an issue when app is in background state, call messages are not handled properly --- Session.xcodeproj/project.pbxproj | 4 ++-- SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 6bb171e14fd..45a8cde1651 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7778,7 +7778,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 504; + CURRENT_PROJECT_VERSION = 505; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7849,7 +7849,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 504; + CURRENT_PROJECT_VERSION = 505; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index 1f11a14570c..dd6ef8a7bd5 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -413,7 +413,9 @@ public class Poller { job: job, canStartJob: ( !forceSynchronousProcessing && - (Singleton.hasAppContext && !Singleton.appContext.isInBackground) + (Singleton.hasAppContext && !Singleton.appContext.isInBackground) || + // FIXME: Better seperate the call messages handling, since we need to handle them all the time + SessionEnvironment.shared?.callManager.wrappedValue?.currentCall != nil ), using: dependencies ) From 2faad168ef80f7d9a52a15bf79e10b2db90cae4f Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Wed, 4 Dec 2024 16:51:41 +1100 Subject: [PATCH 22/33] Potentially fix a keyboard issue for calls --- .../Call Management/SessionCallManager.swift | 3 +-- Session/Calls/CallVC.swift | 7 +++-- .../ConversationVC+Interaction.swift | 26 ++++++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 602536c3b03..1d240f45b3e 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -240,8 +240,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { { let callVC = CallVC(for: call) callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 + conversationVC.hideInputAccessoryView() presentingVC.present(callVC, animated: true, completion: nil) } else if !Preferences.isCallKitSupported { diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index b6b35276757..86d3de51685 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -600,8 +600,11 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { [weak self] _ in - self?.conversationVC?.showInputAccessoryView() - self?.presentingViewController?.dismiss(animated: true, completion: nil) + DispatchQueue.main.async { + self?.presentingViewController?.dismiss(animated: true, completion: { + self?.conversationVC?.showInputAccessoryView() + }) + } } } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ba1669fcb97..e0bb1d24d7d 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -791,17 +791,29 @@ extension ConversationVC: } func hideInputAccessoryView() { - DispatchQueue.main.async { - self.inputAccessoryView?.isHidden = true - self.inputAccessoryView?.alpha = 0 + guard Thread.isMainThread else { + DispatchQueue.main.async { + self.hideInputAccessoryView() + } + return } + self.inputAccessoryView?.isHidden = true + self.inputAccessoryView?.alpha = 0 } func showInputAccessoryView() { - UIView.animate(withDuration: 0.25, animations: { - self.inputAccessoryView?.isHidden = false - self.inputAccessoryView?.alpha = 1 - }) + guard Thread.isMainThread else { + DispatchQueue.main.async { + self.showInputAccessoryView() + } + return + } + self.inputAccessoryView?.isHidden = false + self.inputAccessoryView?.alpha = 1 +// UIView.animate(withDuration: 0.25, animations: { +// self.inputAccessoryView?.isHidden = false +// self.inputAccessoryView?.alpha = 1 +// }) } // MARK: MessageCellDelegate From 12d499a5e576a90f2262167cab0c3531a7127961 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 5 Dec 2024 09:37:40 +1100 Subject: [PATCH 23/33] clean up --- Session/Notifications/PushRegistrationManager.swift | 2 +- SessionUtilitiesKit/JobRunner/JobRunner.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index c14a2013bbd..752bdfeec69 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -323,7 +323,7 @@ public enum PushRegistrationError: Error { call?.callInteractionId = interaction?.id } catch { - SNLog("[Calls] Failed to creat call due to error: \(error)") + SNLog("[Calls] Failed to create call due to error: \(error)") } return call diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 63ae4c5b5fb..7cefce9f8fa 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -1901,7 +1901,6 @@ public extension JobRunner { } static func appDidBecomeActive(using dependencies: Dependencies = Dependencies()) { - SNLog("[JobRunner] appDidBecomeActive") instance.appDidBecomeActive(using: dependencies) } From 15243f326d0fd09218645cdcee5b8e3f25ca9e9e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 5 Dec 2024 09:57:41 +1100 Subject: [PATCH 24/33] Changed the HelpViewModel to just provide the "share" option again --- Session/Settings/HelpViewModel.swift | 112 ++++++++++----------------- 1 file changed, 40 insertions(+), 72 deletions(-) diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index 31eb04bb366..89062aa02ea 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -15,7 +15,6 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa public let navigatableState: NavigatableState = NavigatableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() - private static var documentPickerResult: DocumentPickerResult? // MARK: - Initialization @@ -158,54 +157,41 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa let viewController: UIViewController = Singleton.appContext.frontmostViewController else { return } + #if targetEnvironment(simulator) + // stringlint:ignore_start let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( - title: "helpReportABugExportLogs".localized(), - body: .text("helpReportABugExportLogsDescription" - .put(key: "app_name", value: Constants.app_name) - .localized()), - confirmTitle: "save".localized(), - cancelTitle: "share".localized(), + title: "Export Logs", + body: .text( + "How would you like to export the logs?\n\n(This modal only appears on the Simulator)" + ), + confirmTitle: "Copy Path", + cancelTitle: "Share", cancelStyle: .alert_text, - hasCloseButton: true, - dismissOnConfirm: false, - onConfirm: { modal in - #if targetEnvironment(simulator) - UIPasteboard.general.string = latestLogFilePath - #endif - - modal.dismiss(animated: true) { - HelpViewModel.shareLogsInternal( - viaShareSheet: false, - viewControllerToDismiss: viewControllerToDismiss, - targetView: targetView, - animated: animated, - onShareComplete: onShareComplete - ) - } - }, - onCancel: { modal in - #if targetEnvironment(simulator) - UIPasteboard.general.string = latestLogFilePath - #endif - - modal.dismiss(animated: true) { - HelpViewModel.shareLogsInternal( - viaShareSheet: true, - viewControllerToDismiss: viewControllerToDismiss, - targetView: targetView, - animated: animated, - onShareComplete: onShareComplete - ) - } + onConfirm: { _ in UIPasteboard.general.string = latestLogFilePath }, + onCancel: { _ in + HelpViewModel.shareLogsInternal( + viewControllerToDismiss: viewControllerToDismiss, + targetView: targetView, + animated: animated, + onShareComplete: onShareComplete + ) } ) ) + // stringlint:ignore_stop viewController.present(modal, animated: animated, completion: nil) + #else + HelpViewModel.shareLogsInternal( + viewControllerToDismiss: viewControllerToDismiss, + targetView: targetView, + animated: animated, + onShareComplete: onShareComplete + ) + #endif } private static func shareLogsInternal( - viaShareSheet: Bool, viewControllerToDismiss: UIViewController? = nil, targetView: UIView? = nil, animated: Bool = true, @@ -220,47 +206,29 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa let viewController: UIViewController = Singleton.appContext.frontmostViewController else { return } - let showExportOption: () -> () = { - switch viaShareSheet { - case true: - let shareVC = UIActivityViewController( - activityItems: [ URL(fileURLWithPath: latestLogFilePath) ], - applicationActivities: nil - ) - shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() } - - if UIDevice.current.isIPad { - shareVC.excludedActivityTypes = [] - shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - shareVC.popoverPresentationController?.sourceView = (targetView ?? viewController.view) - shareVC.popoverPresentationController?.sourceRect = (targetView ?? viewController.view).bounds - } - viewController.present(shareVC, animated: animated, completion: nil) - - case false: - // Create and present the document picker - let documentPickerResult: DocumentPickerResult = DocumentPickerResult { _ in - HelpViewModel.documentPickerResult = nil - onShareComplete?() - } - HelpViewModel.documentPickerResult = documentPickerResult - - let documentPicker: UIDocumentPickerViewController = UIDocumentPickerViewController( - forExporting: [URL(fileURLWithPath: latestLogFilePath)] - ) - documentPicker.delegate = documentPickerResult - documentPicker.modalPresentationStyle = .formSheet - viewController.present(documentPicker, animated: animated, completion: nil) + let showShareSheet: () -> () = { + let shareVC = UIActivityViewController( + activityItems: [ URL(fileURLWithPath: latestLogFilePath) ], + applicationActivities: nil + ) + shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() } + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + shareVC.popoverPresentationController?.sourceView = (targetView ?? viewController.view) + shareVC.popoverPresentationController?.sourceRect = (targetView ?? viewController.view).bounds } + viewController.present(shareVC, animated: animated, completion: nil) } guard let viewControllerToDismiss: UIViewController = viewControllerToDismiss else { - showExportOption() + showShareSheet() return } viewControllerToDismiss.dismiss(animated: animated) { - showExportOption() + showShareSheet() } } } From 3f893b1ce04240fb9937141c3874c18edfa611c3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 5 Dec 2024 12:39:58 +1100 Subject: [PATCH 25/33] Cleaned up and finished the export/import logic --- .../Settings/DeveloperSettingsViewModel.swift | 263 +++++++--- Session/Settings/HelpViewModel.swift | 23 - SessionUtilitiesKit/Database/Storage.swift | 108 ++++- .../Utilities/DirectoryArchiver.swift | 456 ++++++++++++------ 4 files changed, 591 insertions(+), 259 deletions(-) diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 9b2572427b8..654dbb9a819 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -200,10 +200,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, explanation: NSAttributedString( string: """ This will generate a file encrypted using the provided password includes all app data, attachments, settings and keys. - - We've generated a secure password for you but feel free to provide your own. + + This exported file can only be imported by Session iOS. Use at your own risk! + + We've generated a secure password for you but feel free to provide your own. """ ), placeholder: "Enter a password", @@ -234,25 +236,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, } private func importDatabase(_ targetView: UIView?) { - func showError(_ error: Error) { - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "theError".localized(), - body: { - switch error { - case CryptoKitError.incorrectKeySize: - return .text("The password must be between 6 and 32 characters (padded to 32 bytes)") - - default: return .text("Failed to export database") - } - }() - ) - ), - transitionType: .present - ) - } - self.databaseKeyEncryptionPassword = "" self.transitionToScreen( ConfirmationModal( @@ -262,6 +245,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, explanation: NSAttributedString( string: """ Importing a database will result in the loss of all data stored locally. + + This can only import backup files exported by Session iOS. Use at your own risk! """ @@ -277,57 +262,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, dismissOnConfirm: false, onConfirm: { [weak self] modal in modal.dismiss(animated: true) { - guard - let password: String = self?.databaseKeyEncryptionPassword, - password.count >= 6 - else { return showError(CryptoKitError.incorrectKeySize) } - - let documentPickerResult: DocumentPickerResult = DocumentPickerResult { url in - guard let url: URL = url else { return } - - let viewController: UIViewController = ModalActivityIndicatorViewController(canCancel: false) { modalActivityIndicator in - do { - let tmpUnencryptPath: String = "\(Singleton.appContext.temporaryDirectory)/new_session.bak" - let extraFilePaths: [String] = try DirectoryArchiver.unarchiveDirectory( - archivePath: url.path, - destinationPath: tmpUnencryptPath, - password: password, - progressChanged: { fileProgress, fileSize in - let percentage: Int = { - guard fileSize > 0 else { return 0 } - - return Int((Double(fileProgress) / Double(fileSize)) * 100) - }() - - DispatchQueue.main.async { - modalActivityIndicator.setMessage( - "Decryption progress: \(percentage)%" - ) - } - } - ) - - // TODO: Need to actually replace the current content then kill the app - // TODO: Might be nice to validate that we have database access to the new database with the key - print("RAWR") - modalActivityIndicator.dismiss { - print("RAWR2") - } - } - catch { showError(error) } - } - - self?.transitionToScreen(viewController, transitionType: .present) - } - self?.documentPickerResult = documentPickerResult - - // UIDocumentPickerModeImport copies to a temp file within our container. - // It uses more memory than "open" but lets us avoid working with security scoped URLs. - let documentPickerVC = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true) - documentPickerVC.delegate = documentPickerResult - documentPickerVC.modalPresentationStyle = .fullScreen - - self?.transitionToScreen(documentPickerVC, transitionType: .present) + self?.performImport() } } ) @@ -349,6 +284,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, switch error { case CryptoKitError.incorrectKeySize: return .text("The password must be between 6 and 32 characters (padded to 32 bytes)") + case is DatabaseError: + return .text("An error occurred finalising pending changes in the database") default: return .text("Failed to export database") } @@ -365,6 +302,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let backupFile: String = "\(Singleton.appContext.temporaryDirectory)/session.bak" do { + /// Perform a full checkpoint to ensure any pending changes are written to the main database file + try dependencies.storage.checkpoint(.truncate) + let secureDbKey: String = try dependencies.storage.secureExportKey( password: databaseKeyEncryptionPassword ) @@ -372,6 +312,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, try DirectoryArchiver.archiveDirectory( sourcePath: FileManager.default.appSharedDataDirectoryPath, destinationPath: backupFile, + filenamesToExclude: [ + ".DS_Store", + "\(Storage.dbFileName)-wal", + "\(Storage.dbFileName)-shm" + ], additionalPaths: [secureDbKey], password: databaseKeyEncryptionPassword, progressChanged: { fileIndex, totalFiles, currentFileProgress, currentFileSize in @@ -394,7 +339,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, } ) } - catch { return showError(error) } + catch { + modalActivityIndicator.dismiss { + showError(error) + } + return + } modalActivityIndicator.dismiss { switch viaShareSheet { @@ -431,6 +381,173 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, self.transitionToScreen(viewController, transitionType: .present) } + + private func performImport() { + func showError(_ error: Error) { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: { + switch error { + case CryptoKitError.incorrectKeySize: + return .text("The password must be between 6 and 32 characters (padded to 32 bytes)") + + case is DatabaseError: return .text("Database key in backup file was invalid.") + default: return .text("\(error)") + } + }() + ) + ), + transitionType: .present + ) + } + + guard databaseKeyEncryptionPassword.count >= 6 else { return showError(CryptoKitError.incorrectKeySize) } + + let documentPickerResult: DocumentPickerResult = DocumentPickerResult { url in + guard let url: URL = url else { return } + + let viewController: UIViewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, password = self.databaseKeyEncryptionPassword, dependencies = self.dependencies] modalActivityIndicator in + do { + let tmpUnencryptPath: String = "\(Singleton.appContext.temporaryDirectory)/new_session.bak" + let (paths, additionalFilePaths): ([String], [String]) = try DirectoryArchiver.unarchiveDirectory( + archivePath: url.path, + destinationPath: tmpUnencryptPath, + password: password, + progressChanged: { filesSaved, totalFiles, fileProgress, fileSize in + let percentage: Int = { + guard fileSize > 0 else { return 0 } + + return Int((Double(fileProgress) / Double(fileSize)) * 100) + }() + + DispatchQueue.main.async { + modalActivityIndicator.setMessage([ + "Decryption progress: \(percentage)%", + "Files imported: \(filesSaved)/\(totalFiles)" + ].compactMap { $0 }.joined(separator: "\n")) + } + } + ) + + /// Test that we actually have valid access to the database + guard + let encKeyPath: String = additionalFilePaths + .first(where: { $0.hasSuffix(Storage.encKeyFilename) }), + let databasePath: String = paths + .first(where: { $0.hasSuffix(Storage.dbFileName) }) + else { throw ArchiveError.unableToFindDatabaseKey } + + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Checking for valid database..." + ) + } + + let testStorage: Storage = try Storage( + testAccessTo: databasePath, + encryptedKeyPath: encKeyPath, + encryptedKeyPassword: password + ) + + guard testStorage.isValid else { + throw ArchiveError.decryptionFailed + } + + /// Now that we have confirmed access to the replacement database we need to + /// stop the current account from doing anything + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Clearing current account data..." + ) + + (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() + } + + dependencies.jobRunner.stopAndClearPendingJobs(using: dependencies) + LibSession.suspendNetworkAccess() + dependencies.storage.suspendDatabaseAccess() + try dependencies.storage.closeDatabase() + + let deleteEnumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( + at: URL( + fileURLWithPath: FileManager.default.appSharedDataDirectoryPath + ), + includingPropertiesForKeys: [.isRegularFileKey] + ) + let fileUrls: [URL] = (deleteEnumerator?.allObjects.compactMap { $0 as? URL } ?? []) + try fileUrls.forEach { url in + /// The database `wal` and `shm` files might not exist anymore at this point + /// so we should only remove files which exist to prevent errors + guard FileManager.default.fileExists(atPath: url.path) else { return } + + try FileManager.default.removeItem(atPath: url.path) + } + + /// Current account data has been removed, we now need to copy over the + /// newly imported data + DispatchQueue.main.async { + modalActivityIndicator.setMessage( + "Moving imported data..." + ) + } + + try paths.forEach { path in + /// Need to ensure the destination directry + let targetPath: String = [ + FileManager.default.appSharedDataDirectoryPath, + path.replacingOccurrences(of: tmpUnencryptPath, with: "") + ].joined() // Already has '/' after 'appSharedDataDirectoryPath' + + try FileManager.default.createDirectory( + atPath: URL(fileURLWithPath: targetPath) + .deletingLastPathComponent() + .path, + withIntermediateDirectories: true + ) + try FileManager.default.moveItem(atPath: path, toPath: targetPath) + } + + /// All of the main files have been moved across, we now need to replace the current database key with + /// the one included in the backup + try dependencies.storage.replaceDatabaseKey(path: encKeyPath, password: password) + + /// The import process has completed so we need to restart the app + DispatchQueue.main.async { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Import Complete", + body: .text("The import completed successfully, Session must be reopened in order to complete the process."), + cancelTitle: "Exit", + cancelStyle: .alert_text, + onCancel: { _ in exit(0) } + ) + ), + transitionType: .present + ) + } + } + catch { + modalActivityIndicator.dismiss { + showError(error) + } + } + } + + self.transitionToScreen(viewController, transitionType: .present) + } + self.documentPickerResult = documentPickerResult + + // UIDocumentPickerModeImport copies to a temp file within our container. + // It uses more memory than "open" but lets us avoid working with security scoped URLs. + let documentPickerVC = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true) + documentPickerVC.delegate = documentPickerResult + documentPickerVC.modalPresentationStyle = .fullScreen + + self.transitionToScreen(documentPickerVC, transitionType: .present) + } } private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index 89062aa02ea..2530fd329d9 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -232,26 +232,3 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa } } } - -private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { - private let onResult: (URL?) -> Void - - init(onResult: @escaping (URL?) -> Void) { - self.onResult = onResult - } - - // MARK: - UIDocumentPickerDelegate - - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - guard let url: URL = urls.first else { - self.onResult(nil) - return - } - - self.onResult(url) - } - - func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { - self.onResult(nil) - } -} diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 57013e1b324..65f87eaef4d 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -15,7 +15,7 @@ public extension KeychainStorage.DataKey { static let dbCipherKeySpec: Self = "G open class Storage { public static let queuePrefix: String = "SessionDatabase" - private static let dbFileName: String = "Session.sqlite" + public static let dbFileName: String = "Session.sqlite" private static let kSQLCipherKeySpecLength: Int = 48 /// If a transaction takes longer than this duration a warning will be logged but the transaction will continue to run @@ -69,6 +69,18 @@ open class Storage { configureDatabase(customWriter: customWriter) } + public init( + testAccessTo databasePath: String, + encryptedKeyPath: String, + encryptedKeyPassword: String + ) throws { + try testAccess( + databasePath: databasePath, + encryptedKeyPath: encryptedKeyPath, + encryptedKeyPassword: encryptedKeyPassword + ) + } + private func configureDatabase(customWriter: DatabaseWriter? = nil) { // Create the database directory if needed and ensure it's protection level is set before attempting to // create the database KeySpec or the database itself @@ -446,6 +458,16 @@ open class Storage { Log.info("[Storage] Database access resumed.") } + public func checkpoint(_ mode: Database.CheckpointMode) throws { + try dbWriter?.writeWithoutTransaction { db in _ = try db.checkpoint(mode) } + } + + public func closeDatabase() throws { + suspendDatabaseAccess() + isValid = false + dbWriter = nil + } + public func resetAllStorage() { isValid = false Storage.internalHasCreatedValidInstance.mutate { $0 = false } @@ -733,6 +755,58 @@ public extension ValueObservation { // MARK: - Debug Convenience public extension Storage { + static let encKeyFilename: String = "key.enc" + + func testAccess( + databasePath: String, + encryptedKeyPath: String, + encryptedKeyPassword: String + ) throws { + /// First we need to ensure we can decrypt the encrypted key file + do { + var tmpKeySpec: Data = try decryptSecureExportedKey( + path: encryptedKeyPath, + password: encryptedKeyPassword + ) + tmpKeySpec.resetBytes(in: 0.. String { var keySpec: Data = try getOrGenerateDatabaseKeySpec() defer { keySpec.resetBytes(in: 0.. Data { + let encKeyBase64: String = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8) + + guard + var passwordData: Data = password.data(using: .utf8), + var encKeyData: Data = Data(base64Encoded: encKeyBase64) + else { throw StorageError.generic } + defer { + // Reset content immediately after use + passwordData.resetBytes(in: 0.. Void)? @@ -42,19 +67,47 @@ public class DirectoryArchiver { // Stream-based directory traversal and compression let enumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( at: sourceUrl, - includingPropertiesForKeys: [.isRegularFileKey] + includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey] ) - let fileUrls: [URL] = (enumerator?.allObjects.compactMap { $0 as? URL } ?? []) - .appending(contentsOf: additionalPaths.map { URL(fileURLWithPath: $0) }) + let fileUrls: [URL] = (enumerator?.allObjects + .compactMap { $0 as? URL } + .filter { url -> Bool in + guard !filenamesToExclude.contains(url.lastPathComponent) else { return false } + guard + let resourceValues = try? url.resourceValues( + forKeys: [.isRegularFileKey, .isDirectoryKey] + ) + else { return true } + + return (resourceValues.isRegularFile == true) + }) + .defaulting(to: []) var index: Int = 0 - progressChanged?(index, fileUrls.count, 0, 0) + progressChanged?(index, (fileUrls.count + additionalPaths.count), 0, 0) + + // Include the archiver version so we can validate compatibility when importing + var version: UInt32 = DirectoryArchiver.version + let versionData: [UInt8] = Array(Data(bytes: &version, count: MemoryLayout.size)) + try write(versionData, to: outputStream, blockSize: UInt8.self, password: password) + + // Store general metadata to help with validation and any other non-file related info + var fileCount: UInt32 = UInt32(fileUrls.count) + var additionalFileCount: UInt32 = UInt32(additionalPaths.count) + let metadata: Data = ( + Data(bytes: &fileCount, count: MemoryLayout.size) + + Data(bytes: &additionalFileCount, count: MemoryLayout.size) + ) + try write(Array(metadata), to: outputStream, blockSize: UInt64.self, password: password) + + // Write the main file content try fileUrls.forEach { url in index += 1 try exportFile( sourcePath: sourcePath, fileURL: url, + customRelativePath: nil, outputStream: outputStream, password: password, index: index, @@ -67,10 +120,12 @@ public class DirectoryArchiver { // Add any extra files which we want to include try additionalPaths.forEach { path in index += 1 - // TODO: Need to fix these so that the path replacement with `sourcePath` isn't busted + + let fileUrl: URL = URL(fileURLWithPath: path) try exportFile( sourcePath: sourcePath, - fileURL: URL(fileURLWithPath: path), + fileURL: fileUrl, + customRelativePath: "_extra/\(fileUrl.lastPathComponent)", outputStream: outputStream, password: password, index: index, @@ -81,103 +136,12 @@ public class DirectoryArchiver { } } - private static func exportFile( - sourcePath: String, - fileURL: URL, - outputStream: OutputStream, - password: String?, - index: Int, - totalFiles: Int, - isExtraFile: Bool, - progressChanged: ((Int, Int, UInt64, UInt64) -> Void)? - ) throws { - guard - let values: URLResourceValues = try? fileURL.resourceValues( - forKeys: [.isRegularFileKey, .fileSizeKey] - ), - values.isRegularFile == true, - var fileSize: UInt64 = values.fileSize.map({ UInt64($0) }) - else { - progressChanged?(index, totalFiles, 1, 1) - return - } - - // Relative path preservation - let relativePath: String = fileURL.path.replacingOccurrences( - of: sourcePath, - with: "" - ).trimmingCharacters(in: CharacterSet(charactersIn: "/")) - - // Write path length and path - let pathData: Data = relativePath.data(using: .utf8)! - var pathLength: UInt32 = UInt32(pathData.count) - var isExtraFile: Bool = isExtraFile - - // Encrypt and write metadata (path length + path data) - let metadata: Data = ( - Data(bytes: &pathLength, count: MemoryLayout.size) + - pathData + - Data(bytes: &fileSize, count: MemoryLayout.size) + - Data(bytes: &isExtraFile, count: MemoryLayout.size) - ) - let processedMetadata: [UInt8] - - switch password { - case .none: processedMetadata = Array(metadata) - case .some(let password): - processedMetadata = try encrypt( - buffer: Array(metadata), - password: password - ) - } - - var blockSize: UInt64 = UInt64(processedMetadata.count) - let blockSizeData: [UInt8] = Array(Data(bytes: &blockSize, count: MemoryLayout.size)) - outputStream.write(blockSizeData, maxLength: blockSizeData.count) - outputStream.write(processedMetadata, maxLength: processedMetadata.count) - - // Stream file contents - guard let inputStream: InputStream = InputStream(url: fileURL) else { - progressChanged?(index, totalFiles, 1, 1) - return - } - - inputStream.open() - defer { inputStream.close() } - - var buffer: [UInt8] = [UInt8](repeating: 0, count: 4096) - var currentFileProcessAmount: UInt64 = 0 - while inputStream.hasBytesAvailable { - let bytesRead: Int = inputStream.read(&buffer, maxLength: buffer.count) - currentFileProcessAmount += UInt64(bytesRead) - progressChanged?(index, totalFiles, currentFileProcessAmount, fileSize) - - if bytesRead > 0 { - let processedBytes: [UInt8] - - switch password { - case .none: processedBytes = buffer - case .some(let password): - processedBytes = try encrypt( - buffer: Array(buffer.prefix(bytesRead)), - password: password - ) - } - - var chunkSize: UInt32 = UInt32(processedBytes.count) - let chunkSizeData: [UInt8] = Array(Data(bytes: &chunkSize, count: MemoryLayout.size)) - outputStream.write(chunkSizeData, maxLength: chunkSizeData.count) - outputStream.write(processedBytes, maxLength: processedBytes.count) - } - } - } - public static func unarchiveDirectory( archivePath: String, destinationPath: String, password: String?, - progressChanged: ((UInt64, UInt64) -> Void)? - ) throws -> [String] { + progressChanged: ((Int, Int, UInt64, UInt64) -> Void)? + ) throws -> (paths: [String], additional: [String]) { // Remove any old imported data as we don't want to muddy the new data if FileManager.default.fileExists(atPath: destinationPath) { try? FileManager.default.removeItem(atPath: destinationPath) @@ -200,38 +164,57 @@ public class DirectoryArchiver { inputStream.open() defer { inputStream.close() } - var extraFilePaths: [String] = [] + // First we need to check the version included in the export is compatible with the current one + let (versionData, _, _): ([UInt8], Int, UInt8) = try read(from: inputStream, password: password) + + guard !versionData.isEmpty else { throw ArchiveError.incompatibleVersion } + + var version: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &version) { versionBuffer in + versionData.copyBytes(to: versionBuffer) + } + + // Retrieve and process the general metadata + var metadataOffset = 0 + let (metadataBytes, _, _): ([UInt8], Int, UInt64) = try read(from: inputStream, password: password) + + guard !metadataBytes.isEmpty else { throw ArchiveError.unarchiveFailed } + + // Extract path length and path + let expectedFileCountRange: Range = metadataOffset..<(metadataOffset + MemoryLayout.size) + var expectedFileCount: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &expectedFileCount) { expectedFileCountBuffer in + metadataBytes.copyBytes(to: expectedFileCountBuffer, from: expectedFileCountRange) + } + metadataOffset += MemoryLayout.size + + let expectedAdditionalFileCountRange: Range = metadataOffset..<(metadataOffset + MemoryLayout.size) + var expectedAdditionalFileCount: UInt32 = 0 + _ = withUnsafeMutableBytes(of: &expectedAdditionalFileCount) { expectedAdditionalFileCountBuffer in + metadataBytes.copyBytes(to: expectedAdditionalFileCountBuffer, from: expectedAdditionalFileCountRange) + } + + var filePaths: [String] = [] + var additionalFilePaths: [String] = [] var fileAmountProcessed: UInt64 = 0 - progressChanged?(0, encryptedFileSize) + progressChanged?(0, Int(expectedFileCount + expectedAdditionalFileCount), 0, encryptedFileSize) while inputStream.hasBytesAvailable { - // Read block size - var blockSizeBytes: [UInt8] = [UInt8](repeating: 0, count: MemoryLayout.size) - let bytesRead: Int = inputStream.read(&blockSizeBytes, maxLength: blockSizeBytes.count) - fileAmountProcessed += UInt64(bytesRead) - progressChanged?(fileAmountProcessed, encryptedFileSize) - - switch bytesRead { - case 0: continue // We have finished reading - case blockSizeBytes.count: break // We have started the next block - default: throw ArchiveError.unarchiveFailed // Invalid - } - - var blockSize: UInt64 = 0 - _ = withUnsafeMutableBytes(of: &blockSize) { blockSizeBuffer in - blockSizeBytes.copyBytes(to: blockSizeBuffer, from: ...size) - } + let (metadata, blockSizeBytesRead, encryptedSize): ([UInt8], Int, UInt64) = try read( + from: inputStream, + password: password + ) + fileAmountProcessed += UInt64(blockSizeBytesRead) + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) - // Read and decrypt metadata - var encryptedMetadata: [UInt8] = [UInt8](repeating: 0, count: Int(blockSize)) - guard inputStream.read(&encryptedMetadata, maxLength: encryptedMetadata.count) == encryptedMetadata.count else { - throw ArchiveError.unarchiveFailed - } + // Stop here if we have finished reading + guard blockSizeBytesRead > 0 else { continue } - let metadata: [UInt8] - switch password { - case .none: metadata = encryptedMetadata - case .some(let password): metadata = try decrypt(buffer: encryptedMetadata, password: password) - } + // Process the metadata var offset = 0 // Extract path length and path @@ -267,8 +250,13 @@ public class DirectoryArchiver { atPath: (fullPath as NSString).deletingLastPathComponent, withIntermediateDirectories: true ) - fileAmountProcessed += UInt64(encryptedMetadata.count) - progressChanged?(fileAmountProcessed, encryptedFileSize) + fileAmountProcessed += encryptedSize + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) // Read and decrypt file content guard let outputStream: OutputStream = OutputStream(toFileAtPath: fullPath, append: false) else { @@ -279,43 +267,65 @@ public class DirectoryArchiver { var remainingFileSize: Int = Int(fileSize) while remainingFileSize > 0 { - // Read chunk size - var chunkSizeBytes: [UInt8] = [UInt8](repeating: 0, count: MemoryLayout.size) - guard inputStream.read(&chunkSizeBytes, maxLength: chunkSizeBytes.count) == chunkSizeBytes.count else { - throw ArchiveError.unarchiveFailed - } - var chunkSize: UInt32 = 0 - _ = withUnsafeMutableBytes(of: &chunkSize) { chunkSizeBuffer in - chunkSizeBytes.copyBytes(to: chunkSizeBuffer, from: ...size) - } - fileAmountProcessed += UInt64(chunkSizeBytes.count) - progressChanged?(fileAmountProcessed, encryptedFileSize) - - // Read the chunk - var chunkBytes: [UInt8] = [UInt8](repeating: 0, count: Int(chunkSize)) - guard inputStream.read(&chunkBytes, maxLength: chunkBytes.count) == chunkBytes.count else { - throw ArchiveError.unarchiveFailed - } - - let processedChunk: [UInt8] - switch password { - case .none: processedChunk = chunkBytes - case .some(let password): processedChunk = try decrypt(buffer: chunkBytes, password: password) - } + let (chunk, chunkSizeBytesRead, encryptedSize): ([UInt8], Int, UInt32) = try read( + from: inputStream, + password: password + ) - outputStream.write(processedChunk, maxLength: processedChunk.count) - remainingFileSize -= processedChunk.count + // Write to the output + outputStream.write(chunk, maxLength: chunk.count) + remainingFileSize -= chunk.count - fileAmountProcessed += UInt64(chunkBytes.count) - progressChanged?(fileAmountProcessed, encryptedFileSize) + // Update the progress + fileAmountProcessed += UInt64(chunkSizeBytesRead + chunk.count) + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) } - if isExtraFile { - extraFilePaths.append(fullPath) + // Store the file path info and update the progress + switch isExtraFile { + case false: filePaths.append(fullPath) + case true: additionalFilePaths.append(fullPath) } + progressChanged?( + (filePaths.count + additionalFilePaths.count), + Int(expectedFileCount + expectedAdditionalFileCount), + fileAmountProcessed, + encryptedFileSize + ) + } + + // Validate that the number of files exported matches the number of paths we got back + let testEnumerator: FileManager.DirectoryEnumerator? = FileManager.default.enumerator( + at: URL(fileURLWithPath: destinationPath), + includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey] + ) + let tempFileUrls: [URL] = (testEnumerator?.allObjects + .compactMap { $0 as? URL } + .filter { url -> Bool in + guard + let resourceValues = try? url.resourceValues( + forKeys: [.isRegularFileKey, .isDirectoryKey] + ) + else { return true } + + return (resourceValues.isRegularFile == true) + }) + .defaulting(to: []) + + guard tempFileUrls.count == (filePaths.count + additionalFilePaths.count) else { + throw ArchiveError.importedFileCountMismatch } + guard + filePaths.count == expectedFileCount && + additionalFilePaths.count == expectedAdditionalFileCount + else { throw ArchiveError.importedFileCountMetadataMismatch } - return extraFilePaths + return (filePaths, additionalFilePaths) } private static func encrypt(buffer: [UInt8], password: String) throws -> [UInt8] { @@ -374,6 +384,132 @@ public class DirectoryArchiver { let decryptedData: Data = try AES.GCM.open(sealedBox, using: symmetricKey) return [UInt8](decryptedData) } + + private static func write( + _ data: [UInt8], + to outputStream: OutputStream, + blockSize: T.Type, + password: String? + ) throws where T: FixedWidthInteger, T: UnsignedInteger { + let processedBytes: [UInt8] + + switch password { + case .none: processedBytes = data + case .some(let password): + processedBytes = try encrypt( + buffer: data, + password: password + ) + } + + var blockSize: T = T(processedBytes.count) + let blockSizeData: [UInt8] = Array(Data(bytes: &blockSize, count: MemoryLayout.size)) + outputStream.write(blockSizeData, maxLength: blockSizeData.count) + outputStream.write(processedBytes, maxLength: processedBytes.count) + } + + private static func read( + from inputStream: InputStream, + password: String? + ) throws -> (value: [UInt8], blockSizeBytesRead: Int, encryptedSize: T) where T: FixedWidthInteger, T: UnsignedInteger { + var blockSizeBytes: [UInt8] = [UInt8](repeating: 0, count: MemoryLayout.size) + let bytesRead: Int = inputStream.read(&blockSizeBytes, maxLength: blockSizeBytes.count) + + switch bytesRead { + case 0: return ([], bytesRead, 0) // We have finished reading + case blockSizeBytes.count: break // We have started the next block + default: throw ArchiveError.unarchiveFailed // Invalid + } + + var blockSize: T = 0 + _ = withUnsafeMutableBytes(of: &blockSize) { blockSizeBuffer in + blockSizeBytes.copyBytes(to: blockSizeBuffer, from: ...size) + } + + var encryptedResult: [UInt8] = [UInt8](repeating: 0, count: Int(blockSize)) + guard inputStream.read(&encryptedResult, maxLength: encryptedResult.count) == encryptedResult.count else { + throw ArchiveError.unarchiveFailed + } + + let result: [UInt8] + switch password { + case .none: result = encryptedResult + case .some(let password): result = try decrypt(buffer: encryptedResult, password: password) + } + + return (result, bytesRead, blockSize) + } + + private static func exportFile( + sourcePath: String, + fileURL: URL, + customRelativePath: String?, + outputStream: OutputStream, + password: String?, + index: Int, + totalFiles: Int, + isExtraFile: Bool, + progressChanged: ((Int, Int, UInt64, UInt64) -> Void)? + ) throws { + guard + let values: URLResourceValues = try? fileURL.resourceValues( + forKeys: [.isRegularFileKey, .fileSizeKey] + ), + values.isRegularFile == true, + var fileSize: UInt64 = values.fileSize.map({ UInt64($0) }) + else { + progressChanged?(index, totalFiles, 1, 1) + return + } + + // Relative path preservation + let relativePath: String = customRelativePath + .defaulting( + to: fileURL.path + .replacingOccurrences(of: sourcePath, with: "") + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) + ) + + // Write path length and path + let pathData: Data = relativePath.data(using: .utf8)! + var pathLength: UInt32 = UInt32(pathData.count) + var isExtraFile: Bool = isExtraFile + + // Encrypt and write metadata (path length + path data) + let metadata: Data = ( + Data(bytes: &pathLength, count: MemoryLayout.size) + + pathData + + Data(bytes: &fileSize, count: MemoryLayout.size) + + Data(bytes: &isExtraFile, count: MemoryLayout.size) + ) + try write(Array(metadata), to: outputStream, blockSize: UInt64.self, password: password) + + // Stream file contents + guard let inputStream: InputStream = InputStream(url: fileURL) else { + progressChanged?(index, totalFiles, 1, 1) + return + } + + inputStream.open() + defer { inputStream.close() } + + var buffer: [UInt8] = [UInt8](repeating: 0, count: 4096) + var currentFileProcessAmount: UInt64 = 0 + while inputStream.hasBytesAvailable { + let bytesRead: Int = inputStream.read(&buffer, maxLength: buffer.count) + currentFileProcessAmount += UInt64(bytesRead) + progressChanged?(index, totalFiles, currentFileProcessAmount, fileSize) + + if bytesRead > 0 { + try write( + Array(buffer.prefix(bytesRead)), + to: outputStream, + blockSize: UInt32.self, + password: password + ) + } + } + } } fileprivate extension InputStream { From b37e6a03db29c52a2106d644f1e2a976df531486 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Thu, 5 Dec 2024 15:02:48 +1100 Subject: [PATCH 26/33] fix a keyboard issue for calls --- Session/Calls/CallVC.swift | 12 ++++++++---- .../Conversations/ConversationVC+Interaction.swift | 10 ++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index 86d3de51685..7ebcafae04a 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -601,7 +601,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { [weak self] _ in DispatchQueue.main.async { - self?.presentingViewController?.dismiss(animated: true, completion: { + self?.dismiss(animated: true, completion: { self?.conversationVC?.showInputAccessoryView() }) } @@ -626,9 +626,13 @@ final class CallVC: UIViewController, VideoPreviewDelegate { AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) } - DispatchQueue.main.async { - self?.conversationVC?.showInputAccessoryView() - self?.presentingViewController?.dismiss(animated: true, completion: nil) + Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in + DispatchQueue.main.async { + self?.dismiss(animated: true, completion: { + self?.conversationVC?.becomeFirstResponder() + self?.conversationVC?.showInputAccessoryView() + }) + } } } } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index e0bb1d24d7d..248c519344c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -808,12 +808,10 @@ extension ConversationVC: } return } - self.inputAccessoryView?.isHidden = false - self.inputAccessoryView?.alpha = 1 -// UIView.animate(withDuration: 0.25, animations: { -// self.inputAccessoryView?.isHidden = false -// self.inputAccessoryView?.alpha = 1 -// }) + UIView.animate(withDuration: 0.25, animations: { + self.inputAccessoryView?.isHidden = false + self.inputAccessoryView?.alpha = 1 + }) } // MARK: MessageCellDelegate From 3002853d17edc860a5ae5078284e2adf64b7b33e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 5 Dec 2024 17:38:44 +1100 Subject: [PATCH 27/33] Updated the package dependencies to point at the STF org --- Session.xcodeproj/project.pbxproj | 20 +++++++++---------- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1c0ac4215c4..63a95bdad45 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -734,6 +734,7 @@ FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */; }; FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD72BD9A2BDF5EEA00CF6CF6 /* Message+Origin.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BD992BDF5EEA00CF6CF6 /* Message+Origin.swift */; }; + FD756BEB2D0181D700BD7199 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FD756BEA2D0181D700BD7199 /* GRDB */; }; FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; }; FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; }; FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; @@ -845,7 +846,6 @@ FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD652CFD6C4E002CDC71 /* Config.swift */; }; FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */; }; FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */; }; - FDC289422C86AB5800020BC2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FDC289412C86AB5800020BC2 /* GRDB */; }; FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */; }; FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; @@ -2208,7 +2208,7 @@ FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */, FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */, FD6A396D2C2D284B00762359 /* YYImage in Frameworks */, - FDC289422C86AB5800020BC2 /* GRDB in Frameworks */, + FD756BEB2D0181D700BD7199 /* GRDB in Frameworks */, FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4762,7 +4762,7 @@ FD6A38EE2C2A641200762359 /* DifferenceKit */, FD6A39652C2D21E400762359 /* libwebp */, FD6A396C2C2D284B00762359 /* YYImage */, - FDC289412C86AB5800020BC2 /* GRDB */, + FD756BEA2D0181D700BD7199 /* GRDB */, ); productName = SessionUtilities; productReference = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; @@ -5071,7 +5071,7 @@ FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */, FD6A39642C2D21E400762359 /* XCRemoteSwiftPackageReference "libwebp-Xcode" */, FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */, - FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */, + FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */, ); productRefGroup = D221A08A169C9E5E00537ABF /* Products */; projectDirPath = ""; @@ -8654,18 +8654,18 @@ }; FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/oxen-io/session-ios-yyimage.git"; + repositoryURL = "https://github.com/session-foundation/session-ios-yyimage"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.1.0; }; }; - FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { + FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/oxen-io/session-grdb-swift.git"; + repositoryURL = "https://github.com/session-foundation/session-grdb-swift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 106.29.2; + minimumVersion = 106.29.3; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -8799,9 +8799,9 @@ package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */; productName = YYImage; }; - FDC289412C86AB5800020BC2 /* GRDB */ = { + FD756BEA2D0181D700BD7199 /* GRDB */ = { isa = XCSwiftPackageProductDependency; - package = FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */; + package = FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */; productName = GRDB; }; FDEF57292C3CF50B00131302 /* WebRTC */ = { diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 97979c57d34..e7f20ae9637 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "53c1182a7d64df9a13a5c0ec4420b2c8da9c4d81d97b2b79b6e8f2946978471b", + "originHash" : "702124fa8d79aa2cee4d496d60bf6b3ad2053e234bcb84c4df5b8f43e95fd8df", "pins" : [ { "identity" : "cocoalumberjack", @@ -85,16 +85,16 @@ { "identity" : "session-grdb-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/oxen-io/session-grdb-swift.git", + "location" : "https://github.com/session-foundation/session-grdb-swift.git", "state" : { - "revision" : "04f480b95263b7c517085100ceb249f879c021d8", + "revision" : "b3643613f1e0f392fa41072ee499da93b4c06b67", "version" : "106.29.3" } }, { "identity" : "session-ios-yyimage", "kind" : "remoteSourceControl", - "location" : "https://github.com/oxen-io/session-ios-yyimage.git", + "location" : "https://github.com/session-foundation/session-ios-yyimage", "state" : { "revision" : "14786afd2523f80be304b377f9dbab6b7904bf02", "version" : "1.1.0" From 406f4cc595ee4e8577964ab7c94254ab2bcc5659 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Fri, 6 Dec 2024 11:20:22 +1100 Subject: [PATCH 28/33] revert some useless message request control message protection logic --- .../MessageReceiver+MessageRequests.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index df61a3b3d07..9b04338db98 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -124,7 +124,7 @@ extension MessageReceiver { } // Update the `didApproveMe` state of the sender - let shouldInsertControlMessage: Bool = try updateContactApprovalStatusIfNeeded( + try updateContactApprovalStatusIfNeeded( db, senderSessionId: senderId, threadId: nil, @@ -147,7 +147,6 @@ extension MessageReceiver { ) } - guard shouldInsertControlMessage else { return } // Notify the user of their approval (Note: This will always appear in the un-blinded thread) // // Note: We want to do this last as it'll mean the un-blinded thread gets updated and the @@ -166,12 +165,12 @@ extension MessageReceiver { ).inserted(db) } - @discardableResult internal static func updateContactApprovalStatusIfNeeded( + internal static func updateContactApprovalStatusIfNeeded( _ db: Database, senderSessionId: String, threadId: String?, using dependencies: Dependencies - ) throws -> Bool { + ) throws { let userPublicKey: String = getUserHexEncodedPublicKey(db) // If the sender of the message was the current user @@ -182,13 +181,13 @@ extension MessageReceiver { let threadId: String = threadId, let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId), !thread.isNoteToSelf(db) - else { return true } + else { return } // Sending a message to someone flags them as approved so create the contact record if // it doesn't exist let contact: Contact = Contact.fetchOrCreate(db, id: threadId) - guard !contact.isApproved else { return false } + guard !contact.isApproved else { return } try? contact.save(db) _ = try? Contact @@ -204,7 +203,7 @@ extension MessageReceiver { // someone without approving them) let contact: Contact = Contact.fetchOrCreate(db, id: senderSessionId) - guard !contact.didApproveMe else { return false } + guard !contact.didApproveMe else { return } try? contact.save(db) _ = try? Contact @@ -215,7 +214,5 @@ extension MessageReceiver { using: dependencies ) } - - return true } } From 5d9b97e35405ea404e0a0891e3bf42eddeec5650 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Dec 2024 10:53:28 +1100 Subject: [PATCH 29/33] Added Lucide as a dependency, added standard font styling --- Session.xcodeproj/project.pbxproj | 39 +++++++ .../xcshareddata/swiftpm/Package.resolved | 11 +- .../Settings.bundle/ThirdPartyLicenses.plist | 21 ++++ SessionUIKit/Style Guide/Fonts.swift | 107 ++++++++++++++++++ .../Utilities/Localization+Style.swift | 6 + 5 files changed, 183 insertions(+), 1 deletion(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 63a95bdad45..18344d2c922 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -687,6 +687,8 @@ FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6C67262CF6EA2300B350A7 /* PushRegistrationManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67252CF6EA1E00B350A7 /* PushRegistrationManagerType.swift */; }; + FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; + FD6DA9D22D0160F10092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9D12D0160F10092085A /* Lucide */; }; FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; @@ -735,6 +737,8 @@ FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD72BD9A2BDF5EEA00CF6CF6 /* Message+Origin.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BD992BDF5EEA00CF6CF6 /* Message+Origin.swift */; }; FD756BEB2D0181D700BD7199 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FD756BEA2D0181D700BD7199 /* GRDB */; }; + FD756BF02D06686500BD7199 /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD756BEF2D06686500BD7199 /* Lucide */; }; + FD756BF22D06687800BD7199 /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD756BF12D06687800BD7199 /* Lucide */; }; FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; }; FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; }; FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; @@ -2170,6 +2174,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FD756BF22D06687800BD7199 /* Lucide in Frameworks */, FD2286712C38D43000BC06F7 /* DifferenceKit in Frameworks */, FD6A396B2C2D284500762359 /* YYImage in Frameworks */, FD37E9EF28A5ED70003AE748 /* SessionUtilitiesKit.framework in Frameworks */, @@ -2238,7 +2243,9 @@ 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */, 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */, 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */, + FD756BF02D06686500BD7199 /* Lucide in Frameworks */, 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */, + FD6DA9D22D0160F10092085A /* Lucide in Frameworks */, B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */, FDEF572A2C3CF50B00131302 /* WebRTC in Frameworks */, 45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */, @@ -2254,6 +2261,7 @@ A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */, A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */, A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */, + FD6DA9CF2D015B440092085A /* Lucide in Frameworks */, A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */, D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */, C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */, @@ -4694,6 +4702,7 @@ packageProductDependencies = ( FD6A396A2C2D284500762359 /* YYImage */, FD2286702C38D43000BC06F7 /* DifferenceKit */, + FD756BF12D06687800BD7199 /* Lucide */, ); productName = SessionUIKit; productReference = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; @@ -4825,6 +4834,9 @@ FD6A39682C2D283A00762359 /* YYImage */, FD2286782C38D4FF00BC06F7 /* DifferenceKit */, FDEF57292C3CF50B00131302 /* WebRTC */, + FD6DA9CE2D015B440092085A /* Lucide */, + FD6DA9D12D0160F10092085A /* Lucide */, + FD756BEF2D06686500BD7199 /* Lucide */, ); productName = RedPhone; productReference = D221A089169C9E5E00537ABF /* Session.app */; @@ -5072,6 +5084,7 @@ FD6A39642C2D21E400762359 /* XCRemoteSwiftPackageReference "libwebp-Xcode" */, FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */, FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */, + FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */, ); productRefGroup = D221A08A169C9E5E00537ABF /* Products */; projectDirPath = ""; @@ -8668,6 +8681,14 @@ minimumVersion = 106.29.3; }; }; + FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/session-foundation/session-lucide.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.468.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -8799,11 +8820,29 @@ package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */; productName = YYImage; }; + FD6DA9CE2D015B440092085A /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + productName = Lucide; + }; + FD6DA9D12D0160F10092085A /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + productName = Lucide; + }; FD756BEA2D0181D700BD7199 /* GRDB */ = { isa = XCSwiftPackageProductDependency; package = FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */; productName = GRDB; }; + FD756BEF2D06686500BD7199 /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + package = FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */; + productName = Lucide; + }; + FD756BF12D06687800BD7199 /* Lucide */ = { + isa = XCSwiftPackageProductDependency; + package = FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */; + productName = Lucide; + }; FDEF57292C3CF50B00131302 /* WebRTC */ = { isa = XCSwiftPackageProductDependency; package = FD6A390E2C2A93CD00762359 /* XCRemoteSwiftPackageReference "WebRTC" */; diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e7f20ae9637..70222e3b912 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "702124fa8d79aa2cee4d496d60bf6b3ad2053e234bcb84c4df5b8f43e95fd8df", + "originHash" : "c57241b796915b0642f9c260463b2d6fd7d5198beafde785c590f3a7d80d31f5", "pins" : [ { "identity" : "cocoalumberjack", @@ -100,6 +100,15 @@ "version" : "1.1.0" } }, + { + "identity" : "session-lucide", + "kind" : "remoteSourceControl", + "location" : "https://github.com/session-foundation/session-lucide.git", + "state" : { + "revision" : "32fdd138a20e828eea376d01e53efb461cc19ebe", + "version" : "0.468.0" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", diff --git a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist index 77921becb6a..1dcc5edf1f3 100644 --- a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist +++ b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist @@ -1022,6 +1022,27 @@ SOFTWARE. Title session-ios-yyimage + + License + ISC License + +Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + Title + session-lucide + License diff --git a/SessionUIKit/Style Guide/Fonts.swift b/SessionUIKit/Style Guide/Fonts.swift index 8fd5213242b..4572cbf4421 100644 --- a/SessionUIKit/Style Guide/Fonts.swift +++ b/SessionUIKit/Style Guide/Fonts.swift @@ -5,6 +5,8 @@ import UIKit import SwiftUI +// MARK: - UIKit + public enum Fonts { public static func spaceMono(ofSize size: CGFloat) -> UIFont { return UIFont(name: "SpaceMono-Regular", size: size)! @@ -15,6 +17,61 @@ public enum Fonts { } } +public extension Fonts { + enum Headings { + public static let H1: UIFont = .boldSystemFont(ofSize: CGFloat(36)) + public static let H2: UIFont = .boldSystemFont(ofSize: CGFloat(32)) + public static let H3: UIFont = .boldSystemFont(ofSize: CGFloat(29)) + public static let H4: UIFont = .boldSystemFont(ofSize: CGFloat(26)) + public static let H5: UIFont = .boldSystemFont(ofSize: CGFloat(23)) + public static let H6: UIFont = .boldSystemFont(ofSize: CGFloat(20)) + public static let H7: UIFont = .boldSystemFont(ofSize: CGFloat(18)) + public static let H8: UIFont = .boldSystemFont(ofSize: CGFloat(16)) + public static let H9: UIFont = .boldSystemFont(ofSize: CGFloat(14)) + + public static func custom(_ size: CGFloat) -> UIFont { + return .boldSystemFont(ofSize: size) + } + } + + enum Body { + public static let extraLargeRegular: UIFont = .systemFont(ofSize: CGFloat(18)) + public static let largeRegular: UIFont = .systemFont(ofSize: CGFloat(16)) + public static let baseRegular: UIFont = .systemFont(ofSize: CGFloat(14)) + public static let smallRegular: UIFont = .systemFont(ofSize: CGFloat(12)) + public static let extraSmallRegular: UIFont = .systemFont(ofSize: CGFloat(11)) + public static let finePrintRegular: UIFont = .systemFont(ofSize: CGFloat(9)) + public static let extraLargeBold: UIFont = .boldSystemFont(ofSize: CGFloat(18)) + public static let largeBold: UIFont = .boldSystemFont(ofSize: CGFloat(16)) + public static let baseBold: UIFont = .boldSystemFont(ofSize: CGFloat(14)) + public static let smallBold: UIFont = .boldSystemFont(ofSize: CGFloat(12)) + public static let extraSmallBold: UIFont = .boldSystemFont(ofSize: CGFloat(11)) + public static let finePrintBold: UIFont = .boldSystemFont(ofSize: CGFloat(9)) + + public static func custom(_ size: CGFloat, bold: Bool = false) -> UIFont { + switch bold { + case true: return .boldSystemFont(ofSize: size) + case false: return .systemFont(ofSize: size) + } + } + } + + enum Display { + public static let extraLarge: UIFont = Fonts.spaceMono(ofSize: CGFloat(18)) + public static let large: UIFont = Fonts.spaceMono(ofSize: CGFloat(16)) + public static let base: UIFont = Fonts.spaceMono(ofSize: CGFloat(14)) + public static let small: UIFont = Fonts.spaceMono(ofSize: CGFloat(12)) + public static let extraSmall: UIFont = Fonts.spaceMono(ofSize: CGFloat(11)) + public static let finePrint: UIFont = Fonts.spaceMono(ofSize: CGFloat(9)) + + public static func custom(_ size: CGFloat) -> UIFont { + return Fonts.spaceMono(ofSize: size) + } + } +} + +// MARK: - SwiftUI + public extension Font { static func spaceMono(size: CGFloat) -> Font { return Font.custom("SpaceMono-Regular", size: size) @@ -24,3 +81,53 @@ public extension Font { return Font.custom("SpaceMono-Bold", size: size) } } + +public extension Font { + enum Headings { + public static let H1: Font = .system(size: CGFloat(36)).bold() + public static let H2: Font = .system(size: CGFloat(32)).bold() + public static let H3: Font = .system(size: CGFloat(29)).bold() + public static let H4: Font = .system(size: CGFloat(26)).bold() + public static let H5: Font = .system(size: CGFloat(23)).bold() + public static let H6: Font = .system(size: CGFloat(20)).bold() + public static let H7: Font = .system(size: CGFloat(18)).bold() + public static let H8: Font = .system(size: CGFloat(16)).bold() + public static let H9: Font = .system(size: CGFloat(14)).bold() + + public static func custom(_ size: CGFloat) -> Font { + return .system(size: size).bold() + } + } + + enum Body { + public static let extraLargeRegular: Font = .system(size: CGFloat(18)) + public static let largeRegular: Font = .system(size: CGFloat(16)) + public static let baseRegular: Font = .system(size: CGFloat(14)) + public static let smallRegular: Font = .system(size: CGFloat(12)) + public static let extraSmallRegular: Font = .system(size: CGFloat(11)) + public static let finePrintRegular: Font = .system(size: CGFloat(9)) + public static let extraLargeBold: Font = .system(size: CGFloat(18)).bold() + public static let largeBold: Font = .system(size: CGFloat(16)).bold() + public static let baseBold: Font = .system(size: CGFloat(14)).bold() + public static let smallBold: Font = .system(size: CGFloat(12)).bold() + public static let extraSmallBold: Font = .system(size: CGFloat(11)).bold() + public static let finePrintBold: Font = .system(size: CGFloat(9)).bold() + + public static func custom(_ size: CGFloat, bold: Bool = false) -> Font { + return .system(size: size, weight: (bold ? .bold : .regular)) + } + } + + enum Display { + public static let extraLarge: Font = .spaceMono(size: CGFloat(18)) + public static let large: Font = .spaceMono(size: CGFloat(16)) + public static let base: Font = .spaceMono(size: CGFloat(14)) + public static let small: Font = .spaceMono(size: CGFloat(12)) + public static let extraSmall: Font = .spaceMono(size: CGFloat(11)) + public static let finePrint: Font = .spaceMono(size: CGFloat(9)) + + public static func custom(_ size: CGFloat) -> Font { + return .spaceMono(size: size) + } + } +} diff --git a/SessionUIKit/Utilities/Localization+Style.swift b/SessionUIKit/Utilities/Localization+Style.swift index 1da248a26a7..03bbfae3497 100644 --- a/SessionUIKit/Utilities/Localization+Style.swift +++ b/SessionUIKit/Utilities/Localization+Style.swift @@ -4,6 +4,7 @@ import UIKit import SessionUtilitiesKit +import Lucide public extension NSAttributedString { /// These are the tags we current support formatting for @@ -19,6 +20,7 @@ public extension NSAttributedString { case underline = "u" case strikethrough = "s" case primaryTheme = "span" + case icon = "icon" // MARK: - Functions @@ -51,6 +53,7 @@ public extension NSAttributedString { case .underline: return [.underlineStyle: NSUnderlineStyle.single.rawValue] case .strikethrough: return [.strikethroughStyle: NSUnderlineStyle.single.rawValue] case .primaryTheme: return [.foregroundColor: ThemeManager.currentTheme.color(for: .sessionButton_text).defaulting(to: ThemeManager.primaryColor.color)] + case .icon: return Lucide.attributes(for: font) } } } @@ -175,6 +178,9 @@ private extension Collection where Element == NSAttributedString.HTMLTag { case .underline: result[.underlineStyle] = NSUnderlineStyle.single.rawValue case .strikethrough: result[.strikethroughStyle] = NSUnderlineStyle.single.rawValue case .primaryTheme: result[.foregroundColor] = ThemeManager.currentTheme.color(for: .sessionButton_text).defaulting(to: ThemeManager.primaryColor.color) + case .icon: + result[.font] = fontWith(Lucide.font(ofSize: (font.pointSize + 1)), traits: []) + result[.baselineOffset] = -Lucide.defaultBaselineOffset } } } From 987866aeb104033ecc780e9ed1a67b86e0b33590 Mon Sep 17 00:00:00 2001 From: stfsession <185467273+stfsession@users.noreply.github.com> Date: Mon, 9 Dec 2024 00:08:04 +0000 Subject: [PATCH 30/33] [Automated] Update translations from Crowdin --- .../Meta/Translations/Localizable.xcstrings | 115 ++++++++++-------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 60878fd72eb..ec8c5a80a45 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -4366,7 +4366,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Hey, I've been using {app_name} to chat with complete privacy and security. Come join me! My Account ID is

{account_id}

Download it at {session_download_url}" + "value" : "مرحبًا، لقد كنت أستخدم {app_name} للدردشة مع خصوصية وأمان كاملين. انضم إليّ! معرف حسابي هو

{account_id}

قم بتحميله من {session_download_url}" } }, "az" : { @@ -25615,7 +25615,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "كبِر" + "value" : "تكبير" } }, "az" : { @@ -32860,7 +32860,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نَزِل المرفق" + "value" : "تنزيل المرفق" } }, "az" : { @@ -46781,7 +46781,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الدقة أو الأبعاد:" + "value" : "دقة الشاشة:" } }, "az" : { @@ -51589,7 +51589,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "غير قادر على تشغيل ملف الصوت." + "value" : "تعذّر تشغيل الملف الصوتي" } }, "az" : { @@ -56894,7 +56894,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تم رفع الحظر عن المستخدم" + "value" : "تم رفع المنع عن المستخدم" } }, "az" : { @@ -57858,7 +57858,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تم حظر المستخدم" + "value" : "تم منع المستخدم" } }, "az" : { @@ -58822,7 +58822,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فك حظر هذه جهة الإتصال لإرسال رسالة" + "value" : "إلغاء حظر جهة الإتصال لإرسال رسالة" } }, "az" : { @@ -117914,7 +117914,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النقر على مفتاح الدخول سوف يرسل الرسالة بدلا من بدء سطر جديد." + "value" : "النقر على Enter سوف يرسل الرسالة بدلاَ من بدء سطر جديد." } }, "az" : { @@ -125141,7 +125141,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أحذف" + "value" : "حذف" } }, "az" : { @@ -154774,7 +154774,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فقط المسؤولين يمكنهم تغيير هذا الإعداد." + "value" : "يمكن لمشرفين المجموعة فقط تغيير هذا الإعداد." } }, "az" : { @@ -168263,7 +168263,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إيموجي & رموز" + "value" : "إيموجي و رموز" } }, "az" : { @@ -170197,7 +170197,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مأكولات & و مشروبات" + "value" : "مأكولات و مشروبات" } }, "az" : { @@ -171634,7 +171634,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ابتسامات & وأشخاص" + "value" : "ابتسامات وأشخاص" } }, "az" : { @@ -172592,7 +172592,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "السفر & و أماكن" + "value" : "السفر و أماكن" } }, "az" : { @@ -178402,7 +178402,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تفاعل مع رسالتك {emoji}" + "value" : "تفاعل مع رسالتك بـ {emoji}" } }, "az" : { @@ -184174,7 +184174,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "صورة GIF" + "value" : "GIF" } }, "az" : { @@ -188018,6 +188018,17 @@ } } }, + "groupDeleteDescriptionMember" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to delete {group_name}?" + } + } + } + }, "groupDeletedMemberDescription" : { "extractionState" : "manual", "localizations" : { @@ -194577,7 +194588,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الدعوة إلى المجموعة ناجحة" + "value" : "تمت دعوة المجموعة بنجاح" } }, "az" : { @@ -207490,7 +207501,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أسم المجموعة الآن '{group_name}." + "value" : "اسم المجموعة الآن '{group_name}." } }, "az" : { @@ -223627,7 +223638,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اصدر السجلات الخاصة بك، ثم ارفع الملف عبر مكتب المساعدة الخاص بـ{app_name}." + "value" : "إصدار السجلات الخاصة بك، ثم رفع الملف عبر مكتب المساعدة الخاص بـ{app_name}." } }, "az" : { @@ -248187,19 +248198,19 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "other" : { @@ -248211,13 +248222,13 @@ "two" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } }, "zero" : { "stringUnit" : { "state" : "translated", - "value" : "%lld عضو" + "value" : "%lld عضو نشط" } } } @@ -250222,7 +250233,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "دعوة المتصلين" + "value" : "دعوة جهات الاتصال" } }, "az" : { @@ -259221,7 +259232,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اعتبرها مقروءة" + "value" : "تحديد كـ \"مقروء\"" } }, "az" : { @@ -259700,7 +259711,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اجعله/ها غير مقروءة" + "value" : "تحديد كـ \"غير مقروء\"" } }, "az" : { @@ -265965,7 +265976,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إرسال رسالة إلى هذه المجموعة سوف يقبل تلقائيًا دعوة المجموعة." + "value" : "بإرسال رسالة إلى هذه المجموعة سوف يقبل تلقائيًا دعوة المجموعة." } }, "az" : { @@ -267402,7 +267413,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إرسال رسالة إلى هذا المستخدم سوف يقبل تلقائيًا طلب الرسالة الخاص به ويكشف عن معرف حسابك." + "value" : "بإرسال رسالة إلى هذا المستخدم سوف يقبل تلقائيًا طلب الرسالة الخاص به ويكشف عن معرف حسابك." } }, "az" : { @@ -270767,7 +270778,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد طلبات رسالة معلقة" + "value" : "لا توجد طلبات مراسلة معلقة" } }, "az" : { @@ -272689,7 +272700,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حدد الرسالة" + "value" : "تحديد رسالة" } }, "az" : { @@ -276030,7 +276041,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اضغط باستمرار لتسجيل رسالة صوتية" + "value" : "اضغط مع الاستمرار لتسجيل رسالة صوتية" } }, "az" : { @@ -277922,7 +277933,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "صغِّر" + "value" : "تصغير" } }, "az" : { @@ -283214,7 +283225,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إخفاء ملاحظة لنفسي" + "value" : "إخفاء \"ملاحظة لنفسي\"" } }, "az" : { @@ -285142,7 +285153,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "المعلومات معروضة في الإشعارات." + "value" : "المعلومات المعروضة في الإشعارات." } }, "az" : { @@ -288495,7 +288506,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اذهب إلى إعدادات تنبيهات الجهاز" + "value" : "اذهب إلى إعدادات إشعارات الجهاز" } }, "az" : { @@ -300458,7 +300469,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نعم" + "value" : "حسناً" } }, "az" : { @@ -317259,7 +317270,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرجى إدخال كلمة السر الحالية" + "value" : "الرجاء إدخال كلمة السر الحالية" } }, "az" : { @@ -317732,7 +317743,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرجى إدخال كلمة السر الجديدة" + "value" : "الرجاء إدخال كلمة السر الجديدة" } }, "az" : { @@ -333994,7 +334005,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ثَبِت المحادثة" + "value" : "تثبيت المحادثة" } }, "az" : { @@ -334952,7 +334963,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ألغِي تثبيت المحادثة" + "value" : "إلغاء تثبيت المحادثة" } }, "az" : { @@ -339748,7 +339759,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "رمز QR هذا لا يحتوي على معرف حساب" + "value" : "رمز QR هذا لا يحتوي على مُعرف حساب" } }, "az" : { @@ -340706,7 +340717,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "امسح رمز الاستجابة السريعة" + "value" : "امسح رمز الاستجابة السريعة QR" } }, "az" : { @@ -351723,7 +351734,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إخفاء كلمة المرور للاسترجاع" + "value" : "إخفاء كلمة مرور الاسترداد" } }, "az" : { @@ -364183,7 +364194,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الرجاء إدخال كملة بحث." + "value" : "الرجاء إدخال كلمة للبحث." } }, "az" : { @@ -364689,7 +364700,7 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "{found_count} من %lld إجابة" + "value" : "{found_count} من %lld مطابقة" } }, "other" : { @@ -364701,7 +364712,7 @@ "two" : { "stringUnit" : { "state" : "translated", - "value" : "{found_count} من %lld مطابقات" + "value" : "{found_count} من %lld مطابقتين" } }, "zero" : { @@ -366688,7 +366699,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لم يتم العثور على أية نتيجة لـ {query}" + "value" : "لم يتم العثور على نتائج لـ {query}" } }, "az" : { @@ -372454,7 +372465,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أُدعُ صديق" + "value" : "دعوة صديق" } }, "az" : { @@ -381064,7 +381075,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اِذهب اِلى صفحة الدعم" + "value" : "الذهاب لصفحة الدعم" } }, "az" : { @@ -394033,7 +394044,7 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "غير قادر على تشغيل الفيديو." + "value" : "تعذر تشغيل الفيديو" } }, "az" : { From 6f2f46715f50ea79d51d61601a8f9cc7b054c856 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Dec 2024 14:41:11 +1100 Subject: [PATCH 31/33] Added basic handling for env vars provided by Appium --- Session/Meta/AppDelegate.swift | 4 +++ .../Settings/DeveloperSettingsViewModel.swift | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 5f1e75595d3..b010b0cbbb0 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -30,6 +30,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Lifecycle func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Just in case we are running automated tests we should process environment variables + // before we do anything else + DeveloperSettingsViewModel.processUnitTestEnvVaraiblesIfNeeded() + Log.info("[AppDelegate] didFinishLaunchingWithOptions called.") startTime = CACurrentMediaTime() diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 654dbb9a819..2b418559f04 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -550,6 +550,31 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, } } +// MARK: - Automated Test Convenience + +extension DeveloperSettingsViewModel { + static func processUnitTestEnvVaraiblesIfNeeded() { +#if targetEnvironment(simulator) + enum EnvironmentVariable: String { + case animationsEnabled + } + + ProcessInfo.processInfo.environment.forEach { key, value in + guard let variable: EnvironmentVariable = EnvironmentVariable(rawValue: key) else { return } + + switch variable { + case .animationsEnabled: + guard value == "false" else { return } + + UIView.setAnimationsEnabled(false) + } + } +#endif + } +} + +// MARK: - DocumentPickerResult + private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { private let onResult: (URL?) -> Void From 1fd673fc243868f764838ca807e55737fee82dc1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Dec 2024 14:43:27 +1100 Subject: [PATCH 32/33] Increased build number --- Session.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 63a95bdad45..c4b3e04c3f0 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7664,7 +7664,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 506; + CURRENT_PROJECT_VERSION = 507; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7743,7 +7743,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 506; + CURRENT_PROJECT_VERSION = 507; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; From cc102edc26d15fc99bb7e5779aec62dd35a7b7f7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 Dec 2024 14:47:40 +1100 Subject: [PATCH 33/33] Fixed a typo --- Session/Meta/AppDelegate.swift | 2 +- Session/Settings/DeveloperSettingsViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index b010b0cbbb0..1f728c06efc 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -32,7 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Just in case we are running automated tests we should process environment variables // before we do anything else - DeveloperSettingsViewModel.processUnitTestEnvVaraiblesIfNeeded() + DeveloperSettingsViewModel.processUnitTestEnvVariablesIfNeeded() Log.info("[AppDelegate] didFinishLaunchingWithOptions called.") startTime = CACurrentMediaTime() diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 2b418559f04..f172b7361ef 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -553,7 +553,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, // MARK: - Automated Test Convenience extension DeveloperSettingsViewModel { - static func processUnitTestEnvVaraiblesIfNeeded() { + static func processUnitTestEnvVariablesIfNeeded() { #if targetEnvironment(simulator) enum EnvironmentVariable: String { case animationsEnabled