Fixed up the remaining reported internal testing issues
Removed the 'readPublisherFlatMap/writePublisherFlatMap' functions as they easily resulted in behaviours which held up database threads Tweaked the logic around starting the open group pollers to avoid an unlikely atomic lock blocks Updated some logic to avoid accessing database read threads for longer than needed Updated the OpenGroupManager to only update the 'seqNo' value for valid messages Cleaned up some double Atomic wrapped instances which had some weird access behaviours Fixed an issue where a database read thread could have been started within a database write thread Fixed an issue where the ReplaySubject might not emit values in some cases
This commit is contained in:
parent
b6328f79b9
commit
6cf7cc42ab
|
@ -6417,7 +6417,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 410;
|
||||
CURRENT_PROJECT_VERSION = 411;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6489,7 +6489,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 410;
|
||||
CURRENT_PROJECT_VERSION = 411;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6554,7 +6554,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 410;
|
||||
CURRENT_PROJECT_VERSION = 411;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6628,7 +6628,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 410;
|
||||
CURRENT_PROJECT_VERSION = 411;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -7536,7 +7536,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 410;
|
||||
CURRENT_PROJECT_VERSION = 411;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7607,7 +7607,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 410;
|
||||
CURRENT_PROJECT_VERSION = 411;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
|
@ -246,11 +246,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
)
|
||||
// Start the timeout timer for the call
|
||||
.handleEvents(receiveOutput: { [weak self] _ in self?.setupTimeoutTimer() })
|
||||
.flatMap { _ in
|
||||
Storage.shared.writePublisherFlatMap { db -> AnyPublisher<Void, Error> in
|
||||
webRTCSession.sendOffer(db, to: sessionId)
|
||||
}
|
||||
}
|
||||
.flatMap { _ in webRTCSession.sendOffer(to: thread) }
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
|
||||
|
@ -431,10 +427,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
let sessionId: String = self.sessionId
|
||||
let webRTCSession: WebRTCSession = self.webRTCSession
|
||||
|
||||
Storage.shared
|
||||
.readPublisherFlatMap { db in
|
||||
webRTCSession.sendOffer(db, to: sessionId, isRestartingICEConnection: true)
|
||||
}
|
||||
guard let thread: SessionThread = Storage.shared.read({ db in try SessionThread.fetchOne(db, id: sessionId) }) else {
|
||||
return
|
||||
}
|
||||
|
||||
webRTCSession
|
||||
.sendOffer(to: thread, isRestartingICEConnection: true)
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
|
|||
publicKey: call.sessionId,
|
||||
threadVariant: .contact,
|
||||
customImageData: nil,
|
||||
profile: Profile.fetchOrCreate(id: call.sessionId),
|
||||
profile: Storage.shared.read { db in Profile.fetchOrCreate(db, id: call.sessionId) },
|
||||
additionalProfile: nil
|
||||
)
|
||||
displayNameLabel.text = call.contactName
|
||||
|
|
|
@ -464,21 +464,18 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController) { _ in
|
||||
Storage.shared
|
||||
.writePublisherFlatMap { db -> AnyPublisher<Void, Error> in
|
||||
if !updatedMemberIds.contains(userPublicKey) {
|
||||
try MessageSender.leave(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
deleteThread: true
|
||||
)
|
||||
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return MessageSender.update(
|
||||
.writePublisher { db in
|
||||
guard updatedMemberIds.contains(userPublicKey) else { return }
|
||||
|
||||
try MessageSender.leave(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
deleteThread: true
|
||||
)
|
||||
|
||||
}
|
||||
.flatMap {
|
||||
MessageSender.update(
|
||||
groupPublicKey: threadId,
|
||||
with: updatedMemberIds,
|
||||
name: updatedName
|
||||
|
|
|
@ -332,10 +332,8 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
let selectedContacts = self.selectedContacts
|
||||
let message: String? = (selectedContacts.count > 20 ? "GROUP_CREATION_PLEASE_WAIT".localized() : nil)
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in
|
||||
Storage.shared
|
||||
.writePublisherFlatMap { db in
|
||||
try MessageSender.createClosedGroup(db, name: name, members: selectedContacts)
|
||||
}
|
||||
MessageSender
|
||||
.createClosedGroup(name: name, members: selectedContacts)
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sinkUntilComplete(
|
||||
|
|
|
@ -1181,7 +1181,12 @@ extension ConversationVC:
|
|||
)
|
||||
}
|
||||
|
||||
func react(_ cellViewModel: MessageViewModel, with emoji: String, remove: Bool) {
|
||||
func react(
|
||||
_ cellViewModel: MessageViewModel,
|
||||
with emoji: String,
|
||||
remove: Bool,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) {
|
||||
guard
|
||||
self.viewModel.threadData.threadIsMessageRequest != true && (
|
||||
cellViewModel.variant == .standardIncoming ||
|
||||
|
@ -1193,7 +1198,7 @@ extension ConversationVC:
|
|||
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
|
||||
let openGroupRoom: String? = self.viewModel.threadData.openGroupRoomToken
|
||||
let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
let recentReactionTimestamps: [Int64] = General.cache.wrappedValue.recentReactionTimestamps
|
||||
let recentReactionTimestamps: [Int64] = dependencies.generalCache.recentReactionTimestamps
|
||||
|
||||
guard
|
||||
recentReactionTimestamps.count < 20 ||
|
||||
|
@ -1211,7 +1216,7 @@ extension ConversationVC:
|
|||
return
|
||||
}
|
||||
|
||||
General.cache.mutate {
|
||||
dependencies.mutableGeneralCache.mutate {
|
||||
$0.recentReactionTimestamps = Array($0.recentReactionTimestamps
|
||||
.suffix(19))
|
||||
.appending(sentTimestamp)
|
||||
|
@ -1522,7 +1527,7 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
Storage.shared
|
||||
.writePublisherFlatMap { db in
|
||||
.writePublisher { db in
|
||||
OpenGroupManager.shared.add(
|
||||
db,
|
||||
roomToken: room,
|
||||
|
@ -1531,6 +1536,15 @@ extension ConversationVC:
|
|||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
.flatMap { successfullyAddedGroup in
|
||||
OpenGroupManager.shared.performInitialRequestsAfterAdd(
|
||||
successfullyAddedGroup: successfullyAddedGroup,
|
||||
roomToken: room,
|
||||
server: server,
|
||||
publicKey: publicKey,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sinkUntilComplete(
|
||||
|
|
|
@ -63,6 +63,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
|
||||
init(threadId: String, threadVariant: SessionThread.Variant, focusedInteractionInfo: Interaction.TimestampInfo?) {
|
||||
typealias InitialData = (
|
||||
currentUserPublicKey: String,
|
||||
initialUnreadInteractionInfo: Interaction.TimestampInfo?,
|
||||
threadIsBlocked: Bool,
|
||||
currentUserIsClosedGroupMember: Bool?,
|
||||
|
@ -73,6 +74,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
let initialData: InitialData? = Storage.shared.read { db -> InitialData in
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// If we have a specified 'focusedInteractionInfo' then use that, otherwise retrieve the oldest
|
||||
// unread interaction and start focused around that one
|
||||
|
@ -94,7 +96,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
let currentUserIsClosedGroupMember: Bool? = (![.legacyGroup, .group].contains(threadVariant) ? nil :
|
||||
GroupMember
|
||||
.filter(groupMember[.groupId] == threadId)
|
||||
.filter(groupMember[.profileId] == getUserHexEncodedPublicKey(db))
|
||||
.filter(groupMember[.profileId] == currentUserPublicKey)
|
||||
.filter(groupMember[.role] == GroupMember.Role.standard)
|
||||
.isNotEmpty(db)
|
||||
)
|
||||
|
@ -112,6 +114,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
)
|
||||
|
||||
return (
|
||||
currentUserPublicKey,
|
||||
initialUnreadInteractionInfo,
|
||||
threadIsBlocked,
|
||||
currentUserIsClosedGroupMember,
|
||||
|
@ -128,7 +131,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
self.threadData = SessionThreadViewModel(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()),
|
||||
threadIsNoteToSelf: (initialData?.currentUserPublicKey == threadId),
|
||||
threadIsBlocked: initialData?.threadIsBlocked,
|
||||
currentUserIsClosedGroupMember: initialData?.currentUserIsClosedGroupMember,
|
||||
openGroupPermissions: initialData?.openGroupPermissions
|
||||
|
@ -141,7 +144,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
|||
// distinct stutter)
|
||||
self.pagedDataObserver = self.setupPagedObserver(
|
||||
for: threadId,
|
||||
userPublicKey: getUserHexEncodedPublicKey(),
|
||||
userPublicKey: (initialData?.currentUserPublicKey ?? getUserHexEncodedPublicKey()),
|
||||
blindedPublicKey: SessionThread.getUserHexEncodedBlindedKey(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant
|
||||
|
|
|
@ -228,7 +228,7 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
// Preparation
|
||||
SessionApp.homeViewController.mutate { $0 = self }
|
||||
|
||||
updateNavBarButtons()
|
||||
updateNavBarButtons(userProfile: self.viewModel.state.userProfile)
|
||||
setUpNavBarSessionHeading()
|
||||
|
||||
// Recovery phrase reminder
|
||||
|
@ -382,7 +382,7 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
}
|
||||
|
||||
if updatedState.userProfile != self.viewModel.state.userProfile {
|
||||
updateNavBarButtons()
|
||||
updateNavBarButtons(userProfile: updatedState.userProfile)
|
||||
}
|
||||
|
||||
// Update the 'view seed' UI
|
||||
|
@ -489,17 +489,17 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
}
|
||||
}
|
||||
|
||||
private func updateNavBarButtons() {
|
||||
private func updateNavBarButtons(userProfile: Profile) {
|
||||
// Profile picture view
|
||||
let profilePictureView = ProfilePictureView(size: .navigation)
|
||||
profilePictureView.accessibilityIdentifier = "User settings"
|
||||
profilePictureView.accessibilityLabel = "User settings"
|
||||
profilePictureView.isAccessibilityElement = true
|
||||
profilePictureView.update(
|
||||
publicKey: getUserHexEncodedPublicKey(),
|
||||
publicKey: userProfile.id,
|
||||
threadVariant: .contact,
|
||||
customImageData: nil,
|
||||
profile: Profile.fetchOrCreateCurrentUser(),
|
||||
profile: userProfile,
|
||||
additionalProfile: nil
|
||||
)
|
||||
|
||||
|
|
|
@ -26,32 +26,39 @@ public class HomeViewModel {
|
|||
let showViewedSeedBanner: Bool
|
||||
let hasHiddenMessageRequests: Bool
|
||||
let unreadMessageRequestThreadCount: Int
|
||||
let userProfile: Profile?
|
||||
|
||||
init(
|
||||
showViewedSeedBanner: Bool = !Storage.shared[.hasViewedSeed],
|
||||
hasHiddenMessageRequests: Bool = Storage.shared[.hasHiddenMessageRequests],
|
||||
unreadMessageRequestThreadCount: Int = 0,
|
||||
userProfile: Profile? = nil
|
||||
) {
|
||||
self.showViewedSeedBanner = showViewedSeedBanner
|
||||
self.hasHiddenMessageRequests = hasHiddenMessageRequests
|
||||
self.unreadMessageRequestThreadCount = unreadMessageRequestThreadCount
|
||||
self.userProfile = userProfile
|
||||
}
|
||||
let userProfile: Profile
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
self.state = State()
|
||||
typealias InitialData = (
|
||||
showViewedSeedBanner: Bool,
|
||||
hasHiddenMessageRequests: Bool,
|
||||
profile: Profile
|
||||
)
|
||||
|
||||
let initialData: InitialData? = Storage.shared.read { db -> InitialData in
|
||||
(
|
||||
!db[.hasViewedSeed],
|
||||
db[.hasHiddenMessageRequests],
|
||||
Profile.fetchOrCreateCurrentUser(db)
|
||||
)
|
||||
}
|
||||
|
||||
self.state = State(
|
||||
showViewedSeedBanner: (initialData?.showViewedSeedBanner ?? true),
|
||||
hasHiddenMessageRequests: (initialData?.hasHiddenMessageRequests ?? false),
|
||||
unreadMessageRequestThreadCount: 0,
|
||||
userProfile: (initialData?.profile ?? Profile.fetchOrCreateCurrentUser())
|
||||
)
|
||||
self.pagedDataObserver = nil
|
||||
|
||||
// Note: Since this references self we need to finish initializing before setting it, we
|
||||
// also want to skip the initial query and trigger it async so that the push animation
|
||||
// doesn't stutter (it should load basically immediately but without this there is a
|
||||
// distinct stutter)
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey()
|
||||
let userPublicKey: String = self.state.userProfile.id
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
self.pagedDataObserver = PagedDatabaseObserver(
|
||||
pagedTable: SessionThread.self,
|
||||
|
|
|
@ -13,7 +13,7 @@ import SignalCoreKit
|
|||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
private static let maxRootViewControllerInitialQueryDuration: TimeInterval = 5
|
||||
private static let maxRootViewControllerInitialQueryDuration: TimeInterval = 10
|
||||
|
||||
var window: UIWindow?
|
||||
var backgroundSnapshotBlockerWindow: UIWindow?
|
||||
|
@ -73,7 +73,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedStartupAlert(calledFrom: .finishLaunching, error: .migrationError(error))
|
||||
DispatchQueue.main.async {
|
||||
self?.showFailedStartupAlert(calledFrom: .finishLaunching, error: .databaseError(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -150,7 +152,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
DispatchQueue.main.async {
|
||||
self?.showFailedStartupAlert(calledFrom: .enterForeground, error: .migrationError(error))
|
||||
self?.showFailedStartupAlert(calledFrom: .enterForeground, error: .databaseError(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -372,8 +374,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
})
|
||||
|
||||
switch error {
|
||||
// Don't offer the 'Restore' option if it was a 'startupFailed' error as a restore is unlikely to
|
||||
// resolve it (most likely the database is locked or the key was somehow lost - safer to get them
|
||||
// to restart and manually reinstall/restore)
|
||||
case .databaseError(StorageError.startupFailed): break
|
||||
|
||||
// Offer the 'Restore' option if it was a migration error
|
||||
case .migrationError:
|
||||
case .databaseError:
|
||||
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
|
||||
if SUKLegacy.hasLegacyDatabaseFile {
|
||||
// Remove the legacy database and any message hashes that have been migrated to the new DB
|
||||
|
@ -402,7 +409,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
switch result {
|
||||
case .failure:
|
||||
self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .failedToRestore)
|
||||
DispatchQueue.main.async {
|
||||
self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .failedToRestore)
|
||||
}
|
||||
|
||||
case .success:
|
||||
self?.completePostMigrationSetup(calledFrom: lifecycleMethod, needsConfigSync: needsConfigSync)
|
||||
|
@ -848,16 +857,15 @@ private enum LifecycleMethod {
|
|||
// MARK: - StartupError
|
||||
|
||||
private enum StartupError: Error {
|
||||
case databaseStartupError
|
||||
case migrationError(Error)
|
||||
case databaseError(Error)
|
||||
case failedToRestore
|
||||
case startupTimeout
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .databaseStartupError: return "DATABASE_STARTUP_FAILED".localized()
|
||||
case .databaseError(StorageError.startupFailed): return "DATABASE_STARTUP_FAILED".localized()
|
||||
case .failedToRestore: return "DATABASE_RESTORE_FAILED".localized()
|
||||
case .migrationError: return "DATABASE_MIGRATION_FAILED".localized()
|
||||
case .databaseError: return "DATABASE_MIGRATION_FAILED".localized()
|
||||
case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "متاسفانه خطایی رخ داده است";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید";
|
||||
"LOADING_CONVERSATIONS" = "درحال بارگزاری پیام ها...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "هنگام بهینهسازی پایگاه داده خطایی روی داد\n\nشما میتوانید گزارشهای برنامه خود را صادر کنید تا بتوانید برای عیبیابی به اشتراک بگذارید یا میتوانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن دادههای قدیمیتر از دو هفته میشود.";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oups, une erreur est survenue";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard";
|
||||
"LOADING_CONVERSATIONS" = "Chargement des conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "Une erreur est survenue pendant l'optimisation de la base de données\n\nVous pouvez exporter votre journal d'application pour le partager et aider à régler le problème ou vous pouvez restaurer votre appareil\n\nAttention : restaurer votre appareil résultera en une perte des données des deux dernières semaines";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Quelque chose s'est mal passé. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Il semble que vous n'avez pas saisi tous les mots. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -414,10 +414,10 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
|
||||
"APP_STARTUP_EXIT" = "Exit";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to be able to share for troubleshooting but to continue to use Session you may need to reinstall it";
|
||||
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
|
|
@ -169,7 +169,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
|
|||
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in
|
||||
Storage.shared
|
||||
.writePublisherFlatMap { db in
|
||||
.writePublisher { db in
|
||||
OpenGroupManager.shared.add(
|
||||
db,
|
||||
roomToken: roomToken,
|
||||
|
@ -178,6 +178,15 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
|
|||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
.flatMap { successfullyAddedGroup in
|
||||
OpenGroupManager.shared.performInitialRequestsAfterAdd(
|
||||
successfullyAddedGroup: successfullyAddedGroup,
|
||||
roomToken: roomToken,
|
||||
server: server,
|
||||
publicKey: publicKey,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sinkUntilComplete(
|
||||
|
|
|
@ -9,7 +9,7 @@ import SessionUIKit
|
|||
final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
private let itemsPerSection: Int = (UIDevice.current.isIPad ? 4 : 2)
|
||||
private var maxWidth: CGFloat
|
||||
private var rooms: [OpenGroupAPI.Room] = [] { didSet { update() } }
|
||||
private var data: [OpenGroupManager.DefaultRoomInfo] = [] { didSet { update() } }
|
||||
private var heightConstraint: NSLayoutConstraint!
|
||||
|
||||
var delegate: OpenGroupSuggestionGridDelegate?
|
||||
|
@ -146,8 +146,13 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
|||
.subscribe(on: DispatchQueue.global(qos: .default))
|
||||
.receive(on: DispatchQueue.main, immediatelyIfMain: true)
|
||||
.sinkUntilComplete(
|
||||
receiveCompletion: { [weak self] _ in self?.update() },
|
||||
receiveValue: { [weak self] rooms in self?.rooms = rooms }
|
||||
receiveCompletion: { [weak self] result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: self?.update()
|
||||
}
|
||||
},
|
||||
receiveValue: { [weak self] roomInfo in self?.data = roomInfo }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -157,7 +162,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
|||
spinner.stopAnimating()
|
||||
spinner.isHidden = true
|
||||
|
||||
let roomCount: CGFloat = CGFloat(min(rooms.count, 8)) // Cap to a maximum of 8 (4 rows of 2)
|
||||
let roomCount: CGFloat = CGFloat(min(data.count, 8)) // Cap to a maximum of 8 (4 rows of 2)
|
||||
let numRows: CGFloat = ceil(roomCount / CGFloat(OpenGroupSuggestionGrid.numHorizontalCells))
|
||||
let height: CGFloat = ((OpenGroupSuggestionGrid.cellHeight * numRows) + ((numRows - 1) * layout.minimumLineSpacing))
|
||||
heightConstraint.constant = height
|
||||
|
@ -184,7 +189,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
|||
|
||||
// If there isn't an even number of items then we want to calculate proper sizing
|
||||
return CGSize(
|
||||
width: Cell.calculatedWith(for: rooms[indexPath.item].name),
|
||||
width: Cell.calculatedWith(for: data[indexPath.item].room.name),
|
||||
height: OpenGroupSuggestionGrid.cellHeight
|
||||
)
|
||||
}
|
||||
|
@ -192,12 +197,12 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
|||
// MARK: - Data Source
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return min(rooms.count, 8) // Cap to a maximum of 8 (4 rows of 2)
|
||||
return min(data.count, 8) // Cap to a maximum of 8 (4 rows of 2)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell: Cell = collectionView.dequeue(type: Cell.self, for: indexPath)
|
||||
cell.room = rooms[indexPath.item]
|
||||
cell.update(with: data[indexPath.item].room, existingImageData: data[indexPath.item].existingImageData)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
@ -205,7 +210,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
|||
// MARK: - Interaction
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
let room = rooms[indexPath.section * itemsPerSection + indexPath.item]
|
||||
let room = data[indexPath.section * itemsPerSection + indexPath.item].room
|
||||
delegate?.join(room)
|
||||
}
|
||||
}
|
||||
|
@ -232,8 +237,6 @@ extension OpenGroupSuggestionGrid {
|
|||
)
|
||||
}
|
||||
|
||||
var room: OpenGroupAPI.Room? { didSet { update() } }
|
||||
|
||||
private lazy var snContentView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.themeBorderColor = .borderSeparator
|
||||
|
@ -307,9 +310,7 @@ extension OpenGroupSuggestionGrid {
|
|||
snContentView.pin(to: self)
|
||||
}
|
||||
|
||||
private func update() {
|
||||
guard let room: OpenGroupAPI.Room = room else { return }
|
||||
|
||||
fileprivate func update(with room: OpenGroupAPI.Room, existingImageData: Data?) {
|
||||
label.text = room.name
|
||||
|
||||
// Only continue if we have a room image
|
||||
|
@ -322,11 +323,13 @@ extension OpenGroupSuggestionGrid {
|
|||
|
||||
Publishers
|
||||
.MergeMany(
|
||||
Storage.shared
|
||||
.readPublisherFlatMap { db in
|
||||
OpenGroupManager
|
||||
.roomImage(db, fileId: imageId, for: room.token, on: OpenGroupAPI.defaultServer)
|
||||
}
|
||||
OpenGroupManager
|
||||
.roomImage(
|
||||
fileId: imageId,
|
||||
for: room.token,
|
||||
on: OpenGroupAPI.defaultServer,
|
||||
existingData: existingImageData
|
||||
)
|
||||
.map { ($0, true) }
|
||||
.eraseToAnyPublisher(),
|
||||
// If we have already received the room image then the above will emit first and
|
||||
|
|
|
@ -220,7 +220,7 @@ final class NukeDataModal: Modal {
|
|||
}
|
||||
}
|
||||
|
||||
private func deleteAllLocalData() {
|
||||
private func deleteAllLocalData(using dependencies: Dependencies = Dependencies()) {
|
||||
// Unregister push notifications if needed
|
||||
let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs]
|
||||
let maybeDeviceToken: String? = UserDefaults.standard[.deviceToken]
|
||||
|
@ -244,7 +244,10 @@ final class NukeDataModal: Modal {
|
|||
UserDefaults.removeAll()
|
||||
|
||||
// Remove the cached key so it gets re-cached on next access
|
||||
General.cache.mutate { $0.encodedPublicKey = nil }
|
||||
dependencies.mutableGeneralCache.mutate {
|
||||
$0.encodedPublicKey = nil
|
||||
$0.recentReactionTimestamps = []
|
||||
}
|
||||
|
||||
// Clear the Snode pool
|
||||
SnodeAPI.clearSnodePool()
|
||||
|
|
|
@ -146,19 +146,13 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
}
|
||||
|
||||
public func sendOffer(
|
||||
_ db: Database,
|
||||
to sessionId: String,
|
||||
to thread: SessionThread,
|
||||
isRestartingICEConnection: Bool = false
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
SNLog("[Calls] Sending offer message.")
|
||||
let uuid: String = self.uuid
|
||||
let mediaConstraints: RTCMediaConstraints = mediaConstraints(isRestartingICEConnection)
|
||||
|
||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
|
||||
return Fail(error: WebRTCSessionError.noThread)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Deferred {
|
||||
Future<Void, Error> { [weak self] resolver in
|
||||
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
|
||||
|
|
|
@ -265,18 +265,6 @@ public extension Profile {
|
|||
)
|
||||
}
|
||||
|
||||
/// Fetches or creates a Profile for the specified user
|
||||
///
|
||||
/// **Note:** This method intentionally does **not** save the newly created Profile,
|
||||
/// it will need to be explicitly saved after calling
|
||||
static func fetchOrCreate(id: String) -> Profile {
|
||||
let exisingProfile: Profile? = Storage.shared.read { db in
|
||||
try Profile.fetchOne(db, id: id)
|
||||
}
|
||||
|
||||
return (exisingProfile ?? defaultFor(id))
|
||||
}
|
||||
|
||||
/// Fetches or creates a Profile for the specified user
|
||||
///
|
||||
/// **Note:** This method intentionally does **not** save the newly created Profile,
|
||||
|
|
|
@ -7,30 +7,15 @@ import Sodium
|
|||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
// MARK: - OGMCacheType
|
||||
|
||||
public protocol OGMCacheType {
|
||||
var defaultRoomsPublisher: AnyPublisher<[OpenGroupAPI.Room], Error>? { get set }
|
||||
var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get set }
|
||||
|
||||
var pollers: [String: OpenGroupAPI.Poller] { get set }
|
||||
var isPolling: Bool { get set }
|
||||
|
||||
var hasPerformedInitialPoll: [String: Bool] { get set }
|
||||
var timeSinceLastPoll: [String: TimeInterval] { get set }
|
||||
|
||||
var pendingChanges: [OpenGroupAPI.PendingChange] { get set }
|
||||
|
||||
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
|
||||
}
|
||||
|
||||
// MARK: - OpenGroupManager
|
||||
|
||||
public final class OpenGroupManager {
|
||||
public typealias DefaultRoomInfo = (room: OpenGroupAPI.Room, existingImageData: Data?)
|
||||
|
||||
// MARK: - Cache
|
||||
|
||||
public class Cache: OGMCacheType {
|
||||
public var defaultRoomsPublisher: AnyPublisher<[OpenGroupAPI.Room], Error>?
|
||||
public class Cache: OGMMutableCacheType {
|
||||
public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error>?
|
||||
public var groupImagePublishers: [String: AnyPublisher<Data, Error>] = [:]
|
||||
|
||||
public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server
|
||||
|
@ -60,10 +45,7 @@ public final class OpenGroupManager {
|
|||
|
||||
// MARK: - Variables
|
||||
|
||||
public static let shared: OpenGroupManager = OpenGroupManager()
|
||||
|
||||
/// Note: This should not be accessed directly but rather via the 'OGMDependencies' type
|
||||
fileprivate let mutableCache: Atomic<OGMCacheType> = Atomic(Cache())
|
||||
public static let shared: OpenGroupManager = OpenGroupManager()
|
||||
|
||||
// MARK: - Polling
|
||||
|
||||
|
@ -87,6 +69,7 @@ public final class OpenGroupManager {
|
|||
}
|
||||
.defaulting(to: [])
|
||||
|
||||
// Update the cache state and re-create all of the pollers
|
||||
dependencies.mutableCache.mutate { cache in
|
||||
cache.isPolling = true
|
||||
cache.pollers = servers
|
||||
|
@ -94,14 +77,9 @@ public final class OpenGroupManager {
|
|||
result[server.lowercased()]?.stop() // Should never occur
|
||||
result[server.lowercased()] = OpenGroupAPI.Poller(for: server.lowercased())
|
||||
}
|
||||
|
||||
// Note: We loop separately here because when the cache is mocked-out for tests it
|
||||
// doesn't actually store the value (meaning the pollers won't be started), but if
|
||||
// we do it in the 'reduce' function, the 'reduce' result will actually store the
|
||||
// poller value resulting in a bunch of OpenGroup pollers running in a way that can't
|
||||
// be stopped during unit tests
|
||||
cache.pollers.forEach { _, poller in poller.startIfNeeded(using: dependencies) }
|
||||
}
|
||||
// Now that the pollers have been created actually start them
|
||||
dependencies.cache.pollers.forEach { _, poller in poller.startIfNeeded(using: dependencies) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,7 +163,7 @@ public final class OpenGroupManager {
|
|||
}
|
||||
|
||||
// First check if there is no poller for the specified server
|
||||
if serverOptions.first(where: { dependencies.cache.pollers[$0] != nil }) == nil {
|
||||
if Set(dependencies.cache.pollers.keys).intersection(serverOptions).isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -209,13 +187,11 @@ public final class OpenGroupManager {
|
|||
publicKey: String,
|
||||
calledFromConfigHandling: Bool,
|
||||
dependencies: OGMDependencies = OGMDependencies()
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
) -> 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, dependencies: dependencies) {
|
||||
SNLog("Ignoring join open group attempt (already joined), user initiated: \(!calledFromConfigHandling)")
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
return false
|
||||
}
|
||||
|
||||
// Store the open group information
|
||||
|
@ -270,72 +246,86 @@ public final class OpenGroupManager {
|
|||
)
|
||||
}
|
||||
|
||||
/// We want to avoid blocking the db write thread so we return a future which resolves once the db transaction completes
|
||||
/// and dispatches the result to another queue, this means that the caller can respond to errors resulting from attepting to
|
||||
/// join the community
|
||||
return Future<Void, Error> { resolver in
|
||||
db.afterNextTransactionNested { _ in
|
||||
OpenGroupAPI.workQueue.async {
|
||||
resolver(Result.success(()))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func performInitialRequestsAfterAdd(
|
||||
successfullyAddedGroup: Bool,
|
||||
roomToken: String,
|
||||
server: String,
|
||||
publicKey: String,
|
||||
calledFromConfigHandling: Bool,
|
||||
dependencies: OGMDependencies = OGMDependencies()
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
guard successfullyAddedGroup else {
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// Store the open group information
|
||||
let targetServer: String = {
|
||||
guard OpenGroupManager.isSessionRunOpenGroup(server: server) else {
|
||||
return server.lowercased()
|
||||
}
|
||||
}
|
||||
.flatMap { _ in
|
||||
dependencies.storage
|
||||
.readPublisher { db in
|
||||
try OpenGroupAPI
|
||||
.preparedCapabilitiesAndRoom(
|
||||
db,
|
||||
for: roomToken,
|
||||
on: targetServer,
|
||||
using: dependencies
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
|
||||
.flatMap { info, response -> Future<Void, Error> in
|
||||
Future<Void, Error> { resolver in
|
||||
dependencies.storage.write { db in
|
||||
// Add the new open group to libSession
|
||||
if !calledFromConfigHandling {
|
||||
try SessionUtil.add(
|
||||
db,
|
||||
server: server,
|
||||
rootToken: roomToken,
|
||||
publicKey: publicKey
|
||||
)
|
||||
}
|
||||
|
||||
// Store the capabilities first
|
||||
OpenGroupManager.handleCapabilities(
|
||||
|
||||
return OpenGroupAPI.defaultServer
|
||||
}()
|
||||
|
||||
return dependencies.storage
|
||||
.readPublisher { db in
|
||||
try OpenGroupAPI
|
||||
.preparedCapabilitiesAndRoom(
|
||||
db,
|
||||
capabilities: response.capabilities.data,
|
||||
on: targetServer
|
||||
)
|
||||
|
||||
// Then the room
|
||||
try OpenGroupManager.handlePollInfo(
|
||||
db,
|
||||
pollInfo: OpenGroupAPI.RoomPollInfo(room: response.room.data),
|
||||
publicKey: publicKey,
|
||||
for: roomToken,
|
||||
on: targetServer,
|
||||
dependencies: dependencies
|
||||
) {
|
||||
resolver(Result.success(()))
|
||||
using: dependencies
|
||||
)
|
||||
}
|
||||
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
|
||||
.flatMap { info, response -> Future<Void, Error> in
|
||||
Future<Void, Error> { resolver in
|
||||
dependencies.storage.write { db in
|
||||
// Add the new open group to libSession
|
||||
if !calledFromConfigHandling {
|
||||
try SessionUtil.add(
|
||||
db,
|
||||
server: server,
|
||||
rootToken: roomToken,
|
||||
publicKey: publicKey
|
||||
)
|
||||
}
|
||||
|
||||
// Store the capabilities first
|
||||
OpenGroupManager.handleCapabilities(
|
||||
db,
|
||||
capabilities: response.capabilities.data,
|
||||
on: targetServer
|
||||
)
|
||||
|
||||
// Then the room
|
||||
try OpenGroupManager.handlePollInfo(
|
||||
db,
|
||||
pollInfo: OpenGroupAPI.RoomPollInfo(room: response.room.data),
|
||||
publicKey: publicKey,
|
||||
for: roomToken,
|
||||
on: targetServer,
|
||||
dependencies: dependencies
|
||||
) {
|
||||
resolver(Result.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.handleEvents(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Failed to join open group.")
|
||||
.handleEvents(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Failed to join open group.")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public func delete(
|
||||
|
@ -534,9 +524,11 @@ public final class OpenGroupManager {
|
|||
// Start the poller if needed
|
||||
if dependencies.cache.pollers[server.lowercased()] == nil {
|
||||
dependencies.mutableCache.mutate {
|
||||
$0.pollers[server.lowercased()]?.stop()
|
||||
$0.pollers[server.lowercased()] = OpenGroupAPI.Poller(for: server.lowercased())
|
||||
$0.pollers[server.lowercased()]?.startIfNeeded(using: dependencies)
|
||||
}
|
||||
|
||||
dependencies.cache.pollers[server.lowercased()]?.startIfNeeded(using: dependencies)
|
||||
}
|
||||
|
||||
/// Start downloading the room image (if we don't have one or it's been updated)
|
||||
|
@ -549,10 +541,10 @@ public final class OpenGroupManager {
|
|||
{
|
||||
OpenGroupManager
|
||||
.roomImage(
|
||||
db,
|
||||
fileId: imageId,
|
||||
for: roomToken,
|
||||
on: server,
|
||||
existingData: openGroup.imageData,
|
||||
using: dependencies
|
||||
)
|
||||
// Note: We need to subscribe and receive on different threads to ensure the
|
||||
|
@ -593,45 +585,26 @@ public final class OpenGroupManager {
|
|||
on server: String,
|
||||
dependencies: OGMDependencies = OGMDependencies()
|
||||
) {
|
||||
// Sorting the messages by server ID before importing them fixes an issue where messages
|
||||
// that quote older messages can't find those older messages
|
||||
guard let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) else {
|
||||
SNLog("Couldn't handle open group messages.")
|
||||
return
|
||||
}
|
||||
|
||||
// Sorting the messages by server ID before importing them fixes an issue where messages
|
||||
// that quote older messages can't find those older messages
|
||||
let sortedMessages: [OpenGroupAPI.Message] = messages
|
||||
.filter { $0.deleted != true }
|
||||
.sorted { lhs, rhs in lhs.id < rhs.id }
|
||||
var messageServerInfoToRemove: [(id: Int64, seqNo: Int64)] = messages
|
||||
.filter { $0.deleted == true }
|
||||
.map { ($0.id, $0.seqNo) }
|
||||
let updateSeqNo: (Database, String, inout Int64, Int64, OGMDependencies) -> () = { db, openGroupId, lastValidSeqNo, seqNo, dependencies in
|
||||
// Only update the data if the 'seqNo' is larger than the lastValidSeqNo (only want it to increase)
|
||||
guard seqNo > lastValidSeqNo else { return }
|
||||
|
||||
// Update the 'openGroupSequenceNumber' value (Note: SOGS V4 uses the 'seqNo' instead of the 'serverId')
|
||||
_ = try? OpenGroup
|
||||
.filter(id: openGroupId)
|
||||
.updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: seqNo))
|
||||
|
||||
// Update pendingChange cache
|
||||
dependencies.mutableCache.mutate {
|
||||
$0.pendingChanges = $0.pendingChanges
|
||||
.filter { $0.seqNo == nil || $0.seqNo! > seqNo }
|
||||
}
|
||||
|
||||
// Update the inout value
|
||||
lastValidSeqNo = seqNo
|
||||
}
|
||||
var largestValidSeqNo: Int64 = openGroup.sequenceNumber
|
||||
|
||||
// Process the messages
|
||||
var lastValidSeqNo: Int64 = -1
|
||||
sortedMessages.forEach { message in
|
||||
if message.base64EncodedData == nil && message.reactions == nil {
|
||||
messageServerInfoToRemove.append((message.id, message.seqNo))
|
||||
|
||||
return updateSeqNo(db, openGroup.id, &lastValidSeqNo, message.seqNo, dependencies)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle messages
|
||||
|
@ -658,7 +631,7 @@ public final class OpenGroupManager {
|
|||
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
|
||||
dependencies: dependencies
|
||||
)
|
||||
updateSeqNo(db, openGroup.id, &lastValidSeqNo, message.seqNo, dependencies)
|
||||
largestValidSeqNo = max(largestValidSeqNo, message.seqNo)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
@ -703,7 +676,7 @@ public final class OpenGroupManager {
|
|||
openGroupMessageServerId: message.id,
|
||||
openGroupReactions: reactions
|
||||
)
|
||||
updateSeqNo(db, openGroup.id, &lastValidSeqNo, message.seqNo, dependencies)
|
||||
largestValidSeqNo = max(largestValidSeqNo, message.seqNo)
|
||||
}
|
||||
catch {
|
||||
SNLog("Couldn't handle open group reactions due to error: \(error).")
|
||||
|
@ -712,17 +685,27 @@ public final class OpenGroupManager {
|
|||
}
|
||||
|
||||
// Handle any deletions that are needed
|
||||
guard !messageServerInfoToRemove.isEmpty else { return }
|
||||
if !messageServerInfoToRemove.isEmpty {
|
||||
let messageServerIdsToRemove: [Int64] = messageServerInfoToRemove.map { $0.id }
|
||||
_ = try? Interaction
|
||||
.filter(Interaction.Columns.threadId == openGroup.threadId)
|
||||
.filter(messageServerIdsToRemove.contains(Interaction.Columns.openGroupServerMessageId))
|
||||
.deleteAll(db)
|
||||
|
||||
// Update the seqNo for deletions
|
||||
largestValidSeqNo = max(largestValidSeqNo, (messageServerInfoToRemove.map({ $0.seqNo }).max() ?? 0))
|
||||
}
|
||||
|
||||
let messageServerIdsToRemove: [Int64] = messageServerInfoToRemove.map { $0.id }
|
||||
_ = try? Interaction
|
||||
.filter(Interaction.Columns.threadId == openGroup.threadId)
|
||||
.filter(messageServerIdsToRemove.contains(Interaction.Columns.openGroupServerMessageId))
|
||||
.deleteAll(db)
|
||||
|
||||
// Update the seqNo for deletions
|
||||
if let lastDeletionSeqNo: Int64 = messageServerInfoToRemove.map({ $0.seqNo }).max() {
|
||||
updateSeqNo(db, openGroup.id, &lastValidSeqNo, lastDeletionSeqNo, dependencies)
|
||||
// Now that we've finished processing all valid message changes we can update the `sequenceNumber` to
|
||||
// the `largestValidSeqNo` value
|
||||
_ = try? OpenGroup
|
||||
.filter(id: openGroup.id)
|
||||
.updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: largestValidSeqNo))
|
||||
|
||||
// Update pendingChange cache based on the `largestValidSeqNo` value
|
||||
dependencies.mutableCache.mutate {
|
||||
$0.pendingChanges = $0.pendingChanges
|
||||
.filter { $0.seqNo == nil || $0.seqNo! > largestValidSeqNo }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1013,14 +996,14 @@ public final class OpenGroupManager {
|
|||
subscribeQueue: OpenGroupAPI.workQueue,
|
||||
receiveQueue: OpenGroupAPI.workQueue
|
||||
)
|
||||
) -> AnyPublisher<[OpenGroupAPI.Room], Error> {
|
||||
) -> AnyPublisher<[DefaultRoomInfo], Error> {
|
||||
// Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again
|
||||
if let existingPublisher: AnyPublisher<[OpenGroupAPI.Room], Error> = dependencies.cache.defaultRoomsPublisher {
|
||||
if let existingPublisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies.cache.defaultRoomsPublisher {
|
||||
return existingPublisher
|
||||
}
|
||||
|
||||
// Try to retrieve the default rooms 8 times
|
||||
let publisher: AnyPublisher<[OpenGroupAPI.Room], Error> = dependencies.storage
|
||||
let publisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies.storage
|
||||
.readPublisher { db in
|
||||
try OpenGroupAPI.preparedCapabilitiesAndRooms(
|
||||
db,
|
||||
|
@ -1032,8 +1015,8 @@ public final class OpenGroupManager {
|
|||
.subscribe(on: dependencies.subscribeQueue, immediatelyIfMain: true)
|
||||
.receive(on: dependencies.receiveQueue, immediatelyIfMain: true)
|
||||
.retry(8)
|
||||
.map { info, response in
|
||||
dependencies.storage.writeAsync { db in
|
||||
.map { info, response -> [DefaultRoomInfo]? in
|
||||
dependencies.storage.write { db -> [DefaultRoomInfo] in
|
||||
// Store the capabilities first
|
||||
OpenGroupManager.handleCapabilities(
|
||||
db,
|
||||
|
@ -1042,8 +1025,8 @@ public final class OpenGroupManager {
|
|||
)
|
||||
|
||||
// Then the rooms
|
||||
response.rooms.data
|
||||
.compactMap { room -> (String, String)? in
|
||||
return response.rooms.data
|
||||
.map { room -> DefaultRoomInfo in
|
||||
// Try to insert an inactive version of the OpenGroup (use 'insert'
|
||||
// rather than 'save' as we want it to fail if the room already exists)
|
||||
do {
|
||||
|
@ -1066,24 +1049,32 @@ public final class OpenGroupManager {
|
|||
}
|
||||
catch {}
|
||||
|
||||
guard let imageId: String = room.imageId else { return nil }
|
||||
// Retrieve existing image data if we have it
|
||||
let existingImageData: Data? = try? OpenGroup
|
||||
.select(.imageData)
|
||||
.filter(id: OpenGroup.idFor(roomToken: room.token, server: OpenGroupAPI.defaultServer))
|
||||
.asRequest(of: Data.self)
|
||||
.fetchOne(db)
|
||||
|
||||
return (imageId, room.token)
|
||||
}
|
||||
.forEach { imageId, roomToken in
|
||||
roomImage(
|
||||
db,
|
||||
fileId: imageId,
|
||||
for: roomToken,
|
||||
on: OpenGroupAPI.defaultServer,
|
||||
using: dependencies
|
||||
)
|
||||
return (room, existingImageData)
|
||||
}
|
||||
}
|
||||
|
||||
return response.rooms.data
|
||||
}
|
||||
.map { ($0 ?? []) }
|
||||
.handleEvents(
|
||||
receiveOutput: { roomInfo in
|
||||
roomInfo.forEach { room, existingImageData in
|
||||
guard let imageId: String = room.imageId else { return }
|
||||
|
||||
roomImage(
|
||||
fileId: imageId,
|
||||
for: room.token,
|
||||
on: OpenGroupAPI.defaultServer,
|
||||
existingData: existingImageData,
|
||||
using: dependencies
|
||||
)
|
||||
}
|
||||
},
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
|
@ -1108,10 +1099,10 @@ public final class OpenGroupManager {
|
|||
}
|
||||
|
||||
@discardableResult public static func roomImage(
|
||||
_ db: Database,
|
||||
fileId: String,
|
||||
for roomToken: String,
|
||||
on server: String,
|
||||
existingData: Data?,
|
||||
using dependencies: OGMDependencies = OGMDependencies(
|
||||
subscribeQueue: .global(qos: .background)
|
||||
)
|
||||
|
@ -1130,16 +1121,12 @@ public final class OpenGroupManager {
|
|||
let now: Date = dependencies.date
|
||||
let timeSinceLastUpdate: TimeInterval = (lastOpenGroupImageUpdate.map { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude)
|
||||
let updateInterval: TimeInterval = (7 * 24 * 60 * 60)
|
||||
let canUseExistingImage: Bool = (
|
||||
server.lowercased() == OpenGroupAPI.defaultServer &&
|
||||
timeSinceLastUpdate < updateInterval
|
||||
)
|
||||
|
||||
if
|
||||
server.lowercased() == OpenGroupAPI.defaultServer,
|
||||
timeSinceLastUpdate < updateInterval,
|
||||
let data = try? OpenGroup
|
||||
.select(.imageData)
|
||||
.filter(id: threadId)
|
||||
.asRequest(of: Data.self)
|
||||
.fetchOne(db)
|
||||
{
|
||||
if canUseExistingImage, let data: Data = existingData {
|
||||
return Just(data)
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -1149,33 +1136,54 @@ public final class OpenGroupManager {
|
|||
return publisher
|
||||
}
|
||||
|
||||
let sendData: OpenGroupAPI.PreparedSendData<Data>
|
||||
|
||||
do {
|
||||
sendData = try OpenGroupAPI
|
||||
.preparedDownloadFile(
|
||||
db,
|
||||
fileId: fileId,
|
||||
from: roomToken,
|
||||
on: server,
|
||||
using: dependencies
|
||||
)
|
||||
}
|
||||
catch {
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// Defer the actual download and run it on a separate thread to avoid blocking the calling thread
|
||||
let publisher: AnyPublisher<Data, Error> = Deferred {
|
||||
Future { resolver in
|
||||
dependencies.subscribeQueue.async {
|
||||
// Hold on to the publisher until it has completed at least once
|
||||
OpenGroupAPI
|
||||
.send(
|
||||
data: sendData,
|
||||
using: dependencies
|
||||
)
|
||||
dependencies.storage
|
||||
.readPublisher { db -> (Data?, OpenGroupAPI.PreparedSendData<Data>?) in
|
||||
if canUseExistingImage {
|
||||
let maybeExistingData: Data? = try? OpenGroup
|
||||
.select(.imageData)
|
||||
.filter(id: threadId)
|
||||
.asRequest(of: Data.self)
|
||||
.fetchOne(db)
|
||||
|
||||
if let existingData: Data = maybeExistingData {
|
||||
return (existingData, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
nil,
|
||||
try OpenGroupAPI
|
||||
.preparedDownloadFile(
|
||||
db,
|
||||
fileId: fileId,
|
||||
from: roomToken,
|
||||
on: server,
|
||||
using: dependencies
|
||||
)
|
||||
)
|
||||
}
|
||||
.flatMap { info in
|
||||
switch info {
|
||||
case (.some(let existingData), _):
|
||||
return Just(existingData)
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
case (_, .some(let sendData)):
|
||||
return OpenGroupAPI.send(data: sendData, using: dependencies)
|
||||
.map { _, imageData in imageData }
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
default:
|
||||
return Fail(error: HTTPError.generic)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
.sinkUntilComplete(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
|
@ -1183,7 +1191,7 @@ public final class OpenGroupManager {
|
|||
case .failure(let error): resolver(Result.failure(error))
|
||||
}
|
||||
},
|
||||
receiveValue: { _, imageData in
|
||||
receiveValue: { imageData in
|
||||
if server.lowercased() == OpenGroupAPI.defaultServer {
|
||||
dependencies.storage.write { db in
|
||||
_ = try OpenGroup
|
||||
|
@ -1216,25 +1224,73 @@ public final class OpenGroupManager {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - OGMCacheType
|
||||
|
||||
public protocol OGMMutableCacheType: OGMCacheType {
|
||||
var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get set }
|
||||
var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get set }
|
||||
|
||||
var pollers: [String: OpenGroupAPI.Poller] { get set }
|
||||
var isPolling: Bool { get set }
|
||||
|
||||
var hasPerformedInitialPoll: [String: Bool] { get set }
|
||||
var timeSinceLastPoll: [String: TimeInterval] { get set }
|
||||
|
||||
var pendingChanges: [OpenGroupAPI.PendingChange] { get set }
|
||||
|
||||
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
|
||||
}
|
||||
|
||||
/// This is a read-only version of the `OGMMutableCacheType` designed to avoid unintentionally mutating the instance in a
|
||||
/// non-thread-safe way
|
||||
public protocol OGMCacheType {
|
||||
var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get }
|
||||
var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get }
|
||||
|
||||
var pollers: [String: OpenGroupAPI.Poller] { get }
|
||||
var isPolling: Bool { get }
|
||||
|
||||
var hasPerformedInitialPoll: [String: Bool] { get }
|
||||
var timeSinceLastPoll: [String: TimeInterval] { get }
|
||||
|
||||
var pendingChanges: [OpenGroupAPI.PendingChange] { get }
|
||||
}
|
||||
|
||||
// MARK: - OGMDependencies
|
||||
|
||||
extension OpenGroupManager {
|
||||
public class OGMDependencies: SMKDependencies {
|
||||
internal var _mutableCache: Atomic<Atomic<OGMCacheType>?>
|
||||
public var mutableCache: Atomic<OGMCacheType> {
|
||||
get { Dependencies.getValueSettingIfNull(&_mutableCache) { OpenGroupManager.shared.mutableCache } }
|
||||
set { _mutableCache.mutate { $0 = newValue } }
|
||||
}
|
||||
/// These should not be accessed directly but rather via an instance of this type
|
||||
private static let _cacheInstance: OGMMutableCacheType = OpenGroupManager.Cache()
|
||||
private static let _cacheInstanceAccessQueue = DispatchQueue(label: "OGMCacheInstanceAccess")
|
||||
|
||||
public var cache: OGMCacheType { return mutableCache.wrappedValue }
|
||||
internal var _mutableCache: Atomic<OGMMutableCacheType?>
|
||||
public var mutableCache: Atomic<OGMMutableCacheType> {
|
||||
get {
|
||||
Dependencies.getMutableValueSettingIfNull(&_mutableCache) {
|
||||
OGMDependencies._cacheInstanceAccessQueue.sync { OGMDependencies._cacheInstance }
|
||||
}
|
||||
}
|
||||
}
|
||||
public var cache: OGMCacheType {
|
||||
get {
|
||||
Dependencies.getValueSettingIfNull(&_mutableCache) {
|
||||
OGMDependencies._cacheInstanceAccessQueue.sync { OGMDependencies._cacheInstance }
|
||||
}
|
||||
}
|
||||
set {
|
||||
guard let mutableValue: OGMMutableCacheType = newValue as? OGMMutableCacheType else { return }
|
||||
|
||||
_mutableCache.mutate { $0 = mutableValue }
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
subscribeQueue: DispatchQueue? = nil,
|
||||
receiveQueue: DispatchQueue? = nil,
|
||||
cache: Atomic<OGMCacheType>? = nil,
|
||||
cache: OGMMutableCacheType? = nil,
|
||||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
generalCache: MutableGeneralCacheType? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
sodium: SodiumType? = nil,
|
||||
|
|
|
@ -8,6 +8,7 @@ public enum OpenGroupAPIError: LocalizedError {
|
|||
case noPublicKey
|
||||
case invalidEmoji
|
||||
case invalidPreparedData
|
||||
case invalidPoll
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
|
@ -16,6 +17,7 @@ public enum OpenGroupAPIError: LocalizedError {
|
|||
case .noPublicKey: return "Couldn't find server public key."
|
||||
case .invalidEmoji: return "The emoji is invalid."
|
||||
case .invalidPreparedData: return "Invalid PreparedSendData provided."
|
||||
case .invalidPoll: return "Poller in invalid state."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ public class SMKDependencies: SSKDependencies {
|
|||
subscribeQueue: DispatchQueue? = nil,
|
||||
receiveQueue: DispatchQueue? = nil,
|
||||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
generalCache: MutableGeneralCacheType? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
sodium: SodiumType? = nil,
|
||||
|
|
|
@ -200,7 +200,7 @@ extension MessageReceiver {
|
|||
// Open groups
|
||||
for openGroupURL in message.openGroups {
|
||||
if let (room, server, publicKey) = SessionUtil.parseCommunity(url: openGroupURL) {
|
||||
OpenGroupManager.shared
|
||||
let successfullyAddedGroup: Bool = OpenGroupManager.shared
|
||||
.add(
|
||||
db,
|
||||
roomToken: room,
|
||||
|
@ -208,7 +208,20 @@ extension MessageReceiver {
|
|||
publicKey: publicKey,
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
.sinkUntilComplete()
|
||||
|
||||
if successfullyAddedGroup {
|
||||
db.afterNextTransactionNested { _ in
|
||||
OpenGroupManager.shared.performInitialRequestsAfterAdd(
|
||||
successfullyAddedGroup: successfullyAddedGroup,
|
||||
roomToken: room,
|
||||
server: server,
|
||||
publicKey: publicKey,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
.subscribe(on: OpenGroupAPI.workQueue)
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,130 +12,130 @@ extension MessageSender {
|
|||
public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:])
|
||||
|
||||
public static func createClosedGroup(
|
||||
_ db: Database,
|
||||
name: String,
|
||||
members: Set<String>
|
||||
) throws -> AnyPublisher<SessionThread, Error> {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
var members: Set<String> = members
|
||||
|
||||
// Generate the group's public key
|
||||
let groupKeyPair: ECKeyPair = Curve25519.generateKeyPair()
|
||||
let groupPublicKey: String = KeyPair(
|
||||
publicKey: groupKeyPair.publicKey.bytes,
|
||||
secretKey: groupKeyPair.privateKey.bytes
|
||||
).hexEncodedPublicKey // Includes the 'SessionId.Prefix.standard' prefix
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
let encryptionKeyPair: ECKeyPair = Curve25519.generateKeyPair()
|
||||
|
||||
// Create the group
|
||||
members.insert(userPublicKey) // Ensure the current user is included in the member list
|
||||
let membersAsData: [Data] = members.map { Data(hex: $0) }
|
||||
let admins: Set<String> = [ userPublicKey ]
|
||||
let adminsAsData: [Data] = admins.map { Data(hex: $0) }
|
||||
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)
|
||||
try ClosedGroup(
|
||||
threadId: groupPublicKey,
|
||||
name: name,
|
||||
formationTimestamp: formationTimestamp
|
||||
).insert(db)
|
||||
|
||||
// Store the key pair
|
||||
let latestKeyPairReceivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
try ClosedGroupKeyPair(
|
||||
threadId: groupPublicKey,
|
||||
publicKey: encryptionKeyPair.publicKey,
|
||||
secretKey: encryptionKeyPair.privateKey,
|
||||
receivedTimestamp: latestKeyPairReceivedTimestamp
|
||||
).insert(db)
|
||||
|
||||
// Create the member objects
|
||||
try admins.forEach { adminId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try members.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
// Update libSession
|
||||
try SessionUtil.add(
|
||||
db,
|
||||
groupPublicKey: groupPublicKey,
|
||||
name: name,
|
||||
latestKeyPairPublicKey: encryptionKeyPair.publicKey,
|
||||
latestKeyPairSecretKey: encryptionKeyPair.privateKey,
|
||||
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
|
||||
disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey),
|
||||
members: members,
|
||||
admins: admins
|
||||
)
|
||||
|
||||
let memberSendData: [MessageSender.PreparedSendData] = try members
|
||||
.map { memberId -> MessageSender.PreparedSendData in
|
||||
try MessageSender.preparedSendData(
|
||||
) -> AnyPublisher<SessionThread, Error> {
|
||||
Storage.shared
|
||||
.writePublisher { db -> (String, SessionThread, [MessageSender.PreparedSendData]) in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
var members: Set<String> = members
|
||||
|
||||
// Generate the group's public key
|
||||
let groupKeyPair: ECKeyPair = Curve25519.generateKeyPair()
|
||||
let groupPublicKey: String = KeyPair(
|
||||
publicKey: groupKeyPair.publicKey.bytes,
|
||||
secretKey: groupKeyPair.privateKey.bytes
|
||||
).hexEncodedPublicKey // Includes the 'SessionId.Prefix.standard' prefix
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
let encryptionKeyPair: ECKeyPair = Curve25519.generateKeyPair()
|
||||
|
||||
// Create the group
|
||||
members.insert(userPublicKey) // Ensure the current user is included in the member list
|
||||
let membersAsData: [Data] = members.map { Data(hex: $0) }
|
||||
let admins: Set<String> = [ userPublicKey ]
|
||||
let adminsAsData: [Data] = admins.map { Data(hex: $0) }
|
||||
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)
|
||||
try ClosedGroup(
|
||||
threadId: groupPublicKey,
|
||||
name: name,
|
||||
formationTimestamp: formationTimestamp
|
||||
).insert(db)
|
||||
|
||||
// Store the key pair
|
||||
let latestKeyPairReceivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
try ClosedGroupKeyPair(
|
||||
threadId: groupPublicKey,
|
||||
publicKey: encryptionKeyPair.publicKey,
|
||||
secretKey: encryptionKeyPair.privateKey,
|
||||
receivedTimestamp: latestKeyPairReceivedTimestamp
|
||||
).insert(db)
|
||||
|
||||
// Create the member objects
|
||||
try admins.forEach { adminId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try members.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: groupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
// Update libSession
|
||||
try SessionUtil.add(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(
|
||||
kind: .new(
|
||||
publicKey: Data(hex: groupPublicKey),
|
||||
name: name,
|
||||
encryptionKeyPair: KeyPair(
|
||||
publicKey: encryptionKeyPair.publicKey.bytes,
|
||||
secretKey: encryptionKeyPair.privateKey.bytes
|
||||
),
|
||||
members: membersAsData,
|
||||
admins: adminsAsData,
|
||||
expirationTimer: 0
|
||||
),
|
||||
// Note: We set this here to ensure the value matches
|
||||
// the 'ClosedGroup' object we created
|
||||
sentTimestampMs: UInt64(floor(formationTimestamp * 1000))
|
||||
),
|
||||
to: .contact(publicKey: memberId),
|
||||
namespace: Message.Destination.contact(publicKey: memberId).defaultNamespace,
|
||||
interactionId: nil
|
||||
groupPublicKey: groupPublicKey,
|
||||
name: name,
|
||||
latestKeyPairPublicKey: encryptionKeyPair.publicKey,
|
||||
latestKeyPairSecretKey: encryptionKeyPair.privateKey,
|
||||
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
|
||||
disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey),
|
||||
members: members,
|
||||
admins: admins
|
||||
)
|
||||
}
|
||||
|
||||
return Publishers
|
||||
.MergeMany(
|
||||
// Send a closed group update message to all members individually
|
||||
memberSendData
|
||||
.map { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||
.appending(
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(
|
||||
.subscribe,
|
||||
for: groupPublicKey,
|
||||
publicKey: userPublicKey
|
||||
|
||||
let memberSendData: [MessageSender.PreparedSendData] = try members
|
||||
.map { memberId -> MessageSender.PreparedSendData in
|
||||
try MessageSender.preparedSendData(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(
|
||||
kind: .new(
|
||||
publicKey: Data(hex: groupPublicKey),
|
||||
name: name,
|
||||
encryptionKeyPair: KeyPair(
|
||||
publicKey: encryptionKeyPair.publicKey.bytes,
|
||||
secretKey: encryptionKeyPair.privateKey.bytes
|
||||
),
|
||||
members: membersAsData,
|
||||
admins: adminsAsData,
|
||||
expirationTimer: 0
|
||||
),
|
||||
// Note: We set this here to ensure the value matches
|
||||
// the 'ClosedGroup' object we created
|
||||
sentTimestampMs: UInt64(floor(formationTimestamp * 1000))
|
||||
),
|
||||
to: .contact(publicKey: memberId),
|
||||
namespace: Message.Destination.contact(publicKey: memberId).defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
.collect()
|
||||
.map { _ in thread }
|
||||
.eraseToAnyPublisher()
|
||||
.handleEvents(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .failure: break
|
||||
case .finished:
|
||||
// Start polling
|
||||
ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey)
|
||||
}
|
||||
|
||||
return (userPublicKey, thread, memberSendData)
|
||||
}
|
||||
.flatMap { userPublicKey, thread, memberSendData in
|
||||
Publishers
|
||||
.MergeMany(
|
||||
// Send a closed group update message to all members individually
|
||||
memberSendData
|
||||
.map { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||
.appending(
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(
|
||||
.subscribe,
|
||||
for: thread.id,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
)
|
||||
)
|
||||
.collect()
|
||||
.map { _ in thread }
|
||||
}
|
||||
.handleEvents(
|
||||
receiveOutput: { thread in
|
||||
// Start polling
|
||||
ClosedGroupPoller.shared.startIfNeeded(for: thread.id)
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -148,7 +148,6 @@ extension MessageSender {
|
|||
///
|
||||
/// The returned promise is fulfilled when the message has been sent to the group.
|
||||
private static func generateAndSendNewEncryptionKeyPair(
|
||||
_ db: Database,
|
||||
targetMembers: Set<String>,
|
||||
userPublicKey: String,
|
||||
allGroupMembers: [GroupMember],
|
||||
|
@ -159,65 +158,62 @@ extension MessageSender {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let newKeyPair: ClosedGroupKeyPair
|
||||
let sendData: MessageSender.PreparedSendData
|
||||
|
||||
do {
|
||||
// Generate the new encryption key pair
|
||||
let legacyNewKeyPair: ECKeyPair = Curve25519.generateKeyPair()
|
||||
newKeyPair = ClosedGroupKeyPair(
|
||||
threadId: closedGroup.threadId,
|
||||
publicKey: legacyNewKeyPair.publicKey,
|
||||
secretKey: legacyNewKeyPair.privateKey,
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
)
|
||||
|
||||
// Distribute it
|
||||
let proto = try SNProtoKeyPair.builder(
|
||||
publicKey: newKeyPair.publicKey,
|
||||
privateKey: newKeyPair.secretKey
|
||||
).build()
|
||||
let plaintext = try proto.serializedData()
|
||||
|
||||
distributingKeyPairs.mutate {
|
||||
$0[closedGroup.id] = ($0[closedGroup.id] ?? [])
|
||||
.appending(newKeyPair)
|
||||
}
|
||||
|
||||
sendData = try MessageSender
|
||||
.preparedSendData(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(
|
||||
kind: .encryptionKeyPair(
|
||||
publicKey: nil,
|
||||
wrappers: targetMembers.map { memberPublicKey in
|
||||
ClosedGroupControlMessage.KeyPairWrapper(
|
||||
publicKey: memberPublicKey,
|
||||
encryptedKeyPair: try MessageSender.encryptWithSessionProtocol(
|
||||
db,
|
||||
plaintext: plaintext,
|
||||
for: memberPublicKey
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup)
|
||||
.defaultNamespace,
|
||||
interactionId: nil
|
||||
return Storage.shared
|
||||
.readPublisher { db -> (ClosedGroupKeyPair, MessageSender.PreparedSendData) in
|
||||
// Generate the new encryption key pair
|
||||
let legacyNewKeyPair: ECKeyPair = Curve25519.generateKeyPair()
|
||||
let newKeyPair: ClosedGroupKeyPair = ClosedGroupKeyPair(
|
||||
threadId: closedGroup.threadId,
|
||||
publicKey: legacyNewKeyPair.publicKey,
|
||||
secretKey: legacyNewKeyPair.privateKey,
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
)
|
||||
}
|
||||
catch {
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return MessageSender.sendImmediate(preparedSendData: sendData)
|
||||
.map { _ in newKeyPair }
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
// Distribute it
|
||||
let proto = try SNProtoKeyPair.builder(
|
||||
publicKey: newKeyPair.publicKey,
|
||||
privateKey: newKeyPair.secretKey
|
||||
).build()
|
||||
let plaintext = try proto.serializedData()
|
||||
|
||||
distributingKeyPairs.mutate {
|
||||
$0[closedGroup.id] = ($0[closedGroup.id] ?? [])
|
||||
.appending(newKeyPair)
|
||||
}
|
||||
|
||||
let sendData: MessageSender.PreparedSendData = try MessageSender
|
||||
.preparedSendData(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(
|
||||
kind: .encryptionKeyPair(
|
||||
publicKey: nil,
|
||||
wrappers: targetMembers.map { memberPublicKey in
|
||||
ClosedGroupControlMessage.KeyPairWrapper(
|
||||
publicKey: memberPublicKey,
|
||||
encryptedKeyPair: try MessageSender.encryptWithSessionProtocol(
|
||||
db,
|
||||
plaintext: plaintext,
|
||||
for: memberPublicKey
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
to: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup),
|
||||
namespace: try Message.Destination
|
||||
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup)
|
||||
.defaultNamespace,
|
||||
interactionId: nil
|
||||
)
|
||||
|
||||
return (newKeyPair, sendData)
|
||||
}
|
||||
.flatMap { newKeyPair, sendData -> AnyPublisher<ClosedGroupKeyPair, Error> in
|
||||
MessageSender.sendImmediate(preparedSendData: sendData)
|
||||
.map { _ in newKeyPair }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.handleEvents(
|
||||
receiveOutput: { newKeyPair in
|
||||
/// Store it **after** having sent out the message to the group
|
||||
|
@ -253,116 +249,110 @@ extension MessageSender {
|
|||
}
|
||||
|
||||
public static func update(
|
||||
_ db: Database,
|
||||
groupPublicKey: String,
|
||||
with members: Set<String>,
|
||||
name: String
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
// Get the group, check preconditions & prepare
|
||||
guard (try? SessionThread.exists(db, id: groupPublicKey)) == true else {
|
||||
SNLog("Can't update nonexistent closed group.")
|
||||
return Fail(error: MessageSenderError.noThread)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: groupPublicKey) else {
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
do {
|
||||
// Update name if needed
|
||||
if name != closedGroup.name {
|
||||
// Update the group
|
||||
_ = try ClosedGroup
|
||||
.filter(id: closedGroup.id)
|
||||
.updateAll(db, ClosedGroup.Columns.name.set(to: name))
|
||||
return Storage.shared
|
||||
.writePublisher { db -> (String, ClosedGroup, [GroupMember], Set<String>) in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
// Notify the user
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: groupPublicKey,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
.nameChange(name: name)
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
// Get the group, check preconditions & prepare
|
||||
guard (try? SessionThread.exists(db, id: groupPublicKey)) == true else {
|
||||
SNLog("Can't update nonexistent closed group.")
|
||||
throw MessageSenderError.noThread
|
||||
}
|
||||
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: groupPublicKey) else {
|
||||
throw MessageSenderError.invalidClosedGroupUpdate
|
||||
}
|
||||
|
||||
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
// Update name if needed
|
||||
if name != closedGroup.name {
|
||||
// Update the group
|
||||
_ = try ClosedGroup
|
||||
.filter(id: closedGroup.id)
|
||||
.updateAll(db, ClosedGroup.Columns.name.set(to: name))
|
||||
|
||||
// Notify the user
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: groupPublicKey,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
.nameChange(name: name)
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
||||
// Send the update to the group
|
||||
try MessageSender.send(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(kind: .nameChange(name: name)),
|
||||
interactionId: interactionId,
|
||||
threadId: groupPublicKey,
|
||||
threadVariant: .legacyGroup
|
||||
)
|
||||
|
||||
// Update libSession
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: closedGroup.threadId,
|
||||
name: name
|
||||
)
|
||||
}
|
||||
|
||||
// Send the update to the group
|
||||
try MessageSender.send(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(kind: .nameChange(name: name)),
|
||||
interactionId: interactionId,
|
||||
threadId: groupPublicKey,
|
||||
threadVariant: .legacyGroup
|
||||
)
|
||||
// Retrieve member info
|
||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
||||
throw MessageSenderError.invalidClosedGroupUpdate
|
||||
}
|
||||
|
||||
// Update libSession
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: closedGroup.threadId,
|
||||
name: name
|
||||
let standardAndZombieMemberIds: [String] = allGroupMembers
|
||||
.filter { $0.role == .standard || $0.role == .zombie }
|
||||
.map { $0.profileId }
|
||||
let addedMembers: Set<String> = members.subtracting(standardAndZombieMemberIds)
|
||||
|
||||
// Add members if needed
|
||||
if !addedMembers.isEmpty {
|
||||
do {
|
||||
try addMembers(
|
||||
db,
|
||||
addedMembers: addedMembers,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
}
|
||||
catch {
|
||||
throw MessageSenderError.invalidClosedGroupUpdate
|
||||
}
|
||||
}
|
||||
|
||||
// Remove members if needed
|
||||
return (
|
||||
userPublicKey,
|
||||
closedGroup,
|
||||
allGroupMembers,
|
||||
Set(standardAndZombieMemberIds).subtracting(members)
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// Retrieve member info
|
||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let standardAndZombieMemberIds: [String] = allGroupMembers
|
||||
.filter { $0.role == .standard || $0.role == .zombie }
|
||||
.map { $0.profileId }
|
||||
let addedMembers: Set<String> = members.subtracting(standardAndZombieMemberIds)
|
||||
|
||||
// Add members if needed
|
||||
if !addedMembers.isEmpty {
|
||||
do {
|
||||
try addMembers(
|
||||
db,
|
||||
addedMembers: addedMembers,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
}
|
||||
catch {
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove members if needed
|
||||
let removedMembers: Set<String> = Set(standardAndZombieMemberIds).subtracting(members)
|
||||
|
||||
if !removedMembers.isEmpty {
|
||||
do {
|
||||
return try removeMembers(
|
||||
db,
|
||||
.flatMap { userPublicKey, closedGroup, allGroupMembers, removedMembers -> AnyPublisher<Void, Error> in
|
||||
guard !removedMembers.isEmpty else {
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return removeMembers(
|
||||
removedMembers: removedMembers,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
.catch { _ in Fail(error: MessageSenderError.invalidClosedGroupUpdate).eraseToAnyPublisher() }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
catch {
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
@ -476,19 +466,20 @@ extension MessageSender {
|
|||
/// The returned promise is fulfilled when the `MEMBERS_REMOVED` message has been sent to the group AND the new encryption key pair has been
|
||||
/// generated and distributed.
|
||||
private static func removeMembers(
|
||||
_ db: Database,
|
||||
removedMembers: Set<String>,
|
||||
userPublicKey: String,
|
||||
allGroupMembers: [GroupMember],
|
||||
closedGroup: ClosedGroup
|
||||
) throws -> AnyPublisher<Void, Error> {
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
guard !removedMembers.contains(userPublicKey) else {
|
||||
SNLog("Invalid closed group update.")
|
||||
throw MessageSenderError.invalidClosedGroupUpdate
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
guard allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) else {
|
||||
SNLog("Only an admin can remove members from a group.")
|
||||
throw MessageSenderError.invalidClosedGroupUpdate
|
||||
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let groupMemberIds: [String] = allGroupMembers
|
||||
|
@ -499,39 +490,39 @@ extension MessageSender {
|
|||
.map { $0.profileId }
|
||||
let members: Set<String> = Set(groupMemberIds).subtracting(removedMembers)
|
||||
|
||||
// Update zombie & member list
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == closedGroup.threadId)
|
||||
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
||||
.filter([ GroupMember.Role.standard, GroupMember.Role.zombie ].contains(GroupMember.Columns.role))
|
||||
.deleteAll(db)
|
||||
|
||||
let interactionId: Int64?
|
||||
|
||||
// Notify the user if needed (not if only zombie members were removed)
|
||||
if !removedMembers.subtracting(groupZombieIds).isEmpty {
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: closedGroup.threadId,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
.membersRemoved(members: removedMembers.map { Data(hex: $0) })
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let newInteractionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
||||
interactionId = newInteractionId
|
||||
}
|
||||
else {
|
||||
interactionId = nil
|
||||
}
|
||||
|
||||
// Send the update to the group and generate + distribute a new encryption key pair
|
||||
return MessageSender
|
||||
.sendImmediate(
|
||||
preparedSendData: try MessageSender
|
||||
return Storage.shared
|
||||
.writePublisher { db in
|
||||
// Update zombie & member list
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == closedGroup.threadId)
|
||||
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
||||
.filter([ GroupMember.Role.standard, GroupMember.Role.zombie ].contains(GroupMember.Columns.role))
|
||||
.deleteAll(db)
|
||||
|
||||
let interactionId: Int64?
|
||||
|
||||
// Notify the user if needed (not if only zombie members were removed)
|
||||
if !removedMembers.subtracting(groupZombieIds).isEmpty {
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: closedGroup.threadId,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
.membersRemoved(members: removedMembers.map { Data(hex: $0) })
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let newInteractionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
||||
interactionId = newInteractionId
|
||||
}
|
||||
else {
|
||||
interactionId = nil
|
||||
}
|
||||
|
||||
// Send the update to the group and generate + distribute a new encryption key pair
|
||||
return try MessageSender
|
||||
.preparedSendData(
|
||||
db,
|
||||
message: ClosedGroupControlMessage(
|
||||
|
@ -546,18 +537,15 @@ extension MessageSender {
|
|||
.defaultNamespace,
|
||||
interactionId: interactionId
|
||||
)
|
||||
)
|
||||
}
|
||||
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||
.flatMap { _ -> AnyPublisher<Void, Error> in
|
||||
Storage.shared
|
||||
.writePublisherFlatMap { db in
|
||||
generateAndSendNewEncryptionKeyPair(
|
||||
db,
|
||||
targetMembers: members,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
}
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(
|
||||
targetMembers: members,
|
||||
userPublicKey: userPublicKey,
|
||||
allGroupMembers: allGroupMembers,
|
||||
closedGroup: closedGroup
|
||||
)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
|
|
@ -57,38 +57,47 @@ extension OpenGroupAPI {
|
|||
) {
|
||||
guard hasStarted else { return }
|
||||
|
||||
let minPollFailureCount: TimeInterval = Storage.shared
|
||||
.read { db in
|
||||
dependencies.storage
|
||||
.readPublisher { [server = server] db in
|
||||
try OpenGroup
|
||||
.filter(OpenGroup.Columns.server == server)
|
||||
.select(min(OpenGroup.Columns.pollFailureCount))
|
||||
.asRequest(of: TimeInterval.self)
|
||||
.fetchOne(db)
|
||||
}
|
||||
.defaulting(to: 0)
|
||||
let lastPollStart: TimeInterval = Date().timeIntervalSince1970
|
||||
let nextPollInterval: TimeInterval = getInterval(for: minPollFailureCount, minInterval: Poller.minPollInterval, maxInterval: Poller.maxPollInterval)
|
||||
|
||||
// Wait until the last poll completes before polling again ensuring we don't poll any faster than
|
||||
// the 'nextPollInterval' value
|
||||
poll(using: dependencies)
|
||||
.tryFlatMap { [weak self] minPollFailureCount -> AnyPublisher<(TimeInterval, TimeInterval), Error> in
|
||||
guard let strongSelf = self else { throw OpenGroupAPIError.invalidPoll }
|
||||
|
||||
let lastPollStart: TimeInterval = Date().timeIntervalSince1970
|
||||
let nextPollInterval: TimeInterval = Poller.getInterval(
|
||||
for: (minPollFailureCount ?? 0),
|
||||
minInterval: Poller.minPollInterval,
|
||||
maxInterval: Poller.maxPollInterval
|
||||
)
|
||||
|
||||
// Wait until the last poll completes before polling again ensuring we don't poll any faster than
|
||||
// the 'nextPollInterval' value
|
||||
return strongSelf.poll(using: dependencies)
|
||||
.map { _ in (lastPollStart, nextPollInterval) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.subscribe(on: dependencies.subscribeQueue, immediatelyIfMain: true)
|
||||
.receive(on: dependencies.receiveQueue, immediatelyIfMain: true)
|
||||
.sinkUntilComplete(
|
||||
receiveCompletion: { [weak self] _ in
|
||||
receiveValue: { [weak self] lastPollStart, nextPollInterval in
|
||||
let currentTime: TimeInterval = Date().timeIntervalSince1970
|
||||
let remainingInterval: TimeInterval = max(0, nextPollInterval - (currentTime - lastPollStart))
|
||||
|
||||
|
||||
guard remainingInterval > 0 else {
|
||||
return Threading.pollerQueue.async {
|
||||
return dependencies.subscribeQueue.async {
|
||||
self?.pollRecursively(using: dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self?.timer = Timer.scheduledTimerOnMainThread(withTimeInterval: remainingInterval, repeats: false) { timer in
|
||||
timer.invalidate()
|
||||
|
||||
Threading.pollerQueue.async {
|
||||
|
||||
dependencies.subscribeQueue.async {
|
||||
self?.pollRecursively(using: dependencies)
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +129,11 @@ extension OpenGroupAPI {
|
|||
|
||||
self.isPolling = true
|
||||
let server: String = self.server
|
||||
let hasPerformedInitialPoll: Bool = (dependencies.cache.hasPerformedInitialPoll[server] == true)
|
||||
let timeSinceLastPoll: TimeInterval = (
|
||||
dependencies.cache.timeSinceLastPoll[server] ??
|
||||
dependencies.mutableCache.mutate { $0.getTimeSinceLastOpen(using: dependencies) }
|
||||
)
|
||||
|
||||
return dependencies.storage
|
||||
.readPublisher { db -> (Int64, PreparedSendData<BatchResponse>) in
|
||||
|
@ -136,11 +150,8 @@ extension OpenGroupAPI {
|
|||
.preparedPoll(
|
||||
db,
|
||||
server: server,
|
||||
hasPerformedInitialPoll: dependencies.cache.hasPerformedInitialPoll[server] == true,
|
||||
timeSinceLastPoll: (
|
||||
dependencies.cache.timeSinceLastPoll[server] ??
|
||||
dependencies.cache.getTimeSinceLastOpen(using: dependencies)
|
||||
),
|
||||
hasPerformedInitialPoll: hasPerformedInitialPoll,
|
||||
timeSinceLastPoll: timeSinceLastPoll,
|
||||
using: dependencies
|
||||
)
|
||||
)
|
||||
|
@ -591,12 +602,12 @@ extension OpenGroupAPI {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
fileprivate static func getInterval(for failureCount: TimeInterval, minInterval: TimeInterval, maxInterval: TimeInterval) -> TimeInterval {
|
||||
// Arbitrary backoff factor...
|
||||
return min(maxInterval, minInterval + pow(2, failureCount))
|
||||
fileprivate static func getInterval(for failureCount: TimeInterval, minInterval: TimeInterval, maxInterval: TimeInterval) -> TimeInterval {
|
||||
// Arbitrary backoff factor...
|
||||
return min(maxInterval, minInterval + pow(2, failureCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@ internal extension SessionUtil {
|
|||
|
||||
// Add any new communities (via the OpenGroupManager)
|
||||
communities.forEach { community in
|
||||
OpenGroupManager.shared
|
||||
let successfullyAddedGroup: Bool = OpenGroupManager.shared
|
||||
.add(
|
||||
db,
|
||||
roomToken: community.data.roomToken,
|
||||
|
@ -146,7 +146,20 @@ internal extension SessionUtil {
|
|||
publicKey: community.data.publicKey,
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
.sinkUntilComplete()
|
||||
|
||||
if successfullyAddedGroup {
|
||||
db.afterNextTransactionNested { _ in
|
||||
OpenGroupManager.shared.performInitialRequestsAfterAdd(
|
||||
successfullyAddedGroup: successfullyAddedGroup,
|
||||
roomToken: community.data.roomToken,
|
||||
server: community.data.server,
|
||||
publicKey: community.data.publicKey,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
.subscribe(on: OpenGroupAPI.workQueue)
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
// Set the priority if it's changed (new communities will have already been inserted at
|
||||
// this stage)
|
||||
|
|
|
@ -403,40 +403,35 @@ public enum SessionUtil {
|
|||
guard SessionUtil.userConfigsEnabled else { return [] }
|
||||
|
||||
return Storage.shared
|
||||
.read { db -> [String] in
|
||||
.read { db -> Set<ConfigDump.Variant> in
|
||||
guard Identity.userExists(db) else { return [] }
|
||||
|
||||
let existingDumpVariants: Set<ConfigDump.Variant> = (try? ConfigDump
|
||||
return try ConfigDump
|
||||
.select(.variant)
|
||||
.filter(ConfigDump.Columns.publicKey == publicKey)
|
||||
.asRequest(of: ConfigDump.Variant.self)
|
||||
.fetchSet(db))
|
||||
.defaulting(to: [])
|
||||
|
||||
/// Extract all existing hashes for any dumps associated with the given `publicKey`
|
||||
return existingDumpVariants
|
||||
.map { variant -> [String] in
|
||||
guard
|
||||
let conf = SessionUtil
|
||||
.config(for: variant, publicKey: publicKey)
|
||||
.wrappedValue,
|
||||
let hashList: UnsafeMutablePointer<config_string_list> = config_current_hashes(conf)
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
let result: [String] = [String](
|
||||
pointer: hashList.pointee.value,
|
||||
count: hashList.pointee.len,
|
||||
defaultValue: []
|
||||
)
|
||||
hashList.deallocate()
|
||||
|
||||
return result
|
||||
}
|
||||
.reduce([], +)
|
||||
.fetchSet(db)
|
||||
}
|
||||
.defaulting(to: [])
|
||||
.map { variant -> [String] in
|
||||
/// Extract all existing hashes for any dumps associated with the given `publicKey`
|
||||
guard
|
||||
let conf = SessionUtil
|
||||
.config(for: variant, publicKey: publicKey)
|
||||
.wrappedValue,
|
||||
let hashList: UnsafeMutablePointer<config_string_list> = config_current_hashes(conf)
|
||||
else { return [] }
|
||||
|
||||
let result: [String] = [String](
|
||||
pointer: hashList.pointee.value,
|
||||
count: hashList.pointee.len,
|
||||
defaultValue: []
|
||||
)
|
||||
hashList.deallocate()
|
||||
|
||||
return result
|
||||
}
|
||||
.reduce([], +)
|
||||
}
|
||||
|
||||
// MARK: - Receiving
|
||||
|
|
|
@ -498,7 +498,7 @@ public struct ProfileManager {
|
|||
dependencies: Dependencies = Dependencies()
|
||||
) throws {
|
||||
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey(db, dependencies: dependencies))
|
||||
let profile: Profile = Profile.fetchOrCreate(id: publicKey)
|
||||
let profile: Profile = Profile.fetchOrCreate(db, id: publicKey)
|
||||
var profileChanges: [ConfigColumnAssignment] = []
|
||||
|
||||
// Name
|
||||
|
|
|
@ -118,9 +118,9 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
dependencies = OpenGroupManager.OGMDependencies(
|
||||
subscribeQueue: DispatchQueue.main,
|
||||
receiveQueue: DispatchQueue.main,
|
||||
cache: Atomic(mockOGMCache),
|
||||
cache: mockOGMCache,
|
||||
onionApi: TestCapabilitiesAndRoomApi.self,
|
||||
generalCache: Atomic(mockGeneralCache),
|
||||
generalCache: mockGeneralCache,
|
||||
storage: mockStorage,
|
||||
sodium: mockSodium,
|
||||
genericHash: mockGenericHash,
|
||||
|
|
|
@ -6,9 +6,9 @@ import SessionUtilitiesKit
|
|||
|
||||
@testable import SessionMessagingKit
|
||||
|
||||
class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
|
||||
var defaultRoomsPublisher: AnyPublisher<[OpenGroupAPI.Room], Error>? {
|
||||
get { return accept() as? AnyPublisher<[OpenGroupAPI.Room], Error> }
|
||||
class MockOGMCache: Mock<OGMMutableCacheType>, OGMMutableCacheType {
|
||||
var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? {
|
||||
get { return accept() as? AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> }
|
||||
set { accept(args: [newValue]) }
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ open class SSKDependencies: Dependencies {
|
|||
subscribeQueue: DispatchQueue? = nil,
|
||||
receiveQueue: DispatchQueue? = nil,
|
||||
onionApi: OnionRequestAPIType.Type? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
generalCache: MutableGeneralCacheType? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
standardUserDefaults: UserDefaultsType? = nil,
|
||||
|
|
|
@ -49,10 +49,23 @@ public final class ReplaySubject<Output, Failure: Error>: Subject {
|
|||
public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
|
||||
let subscription = ReplaySubjectSubscription<Output, Failure>(downstream: AnySubscriber(subscriber))
|
||||
/// According to the below comment the `subscriber.receive(subscription: subscription)` code runs asynchronously
|
||||
/// which aligns with testing (resulting in the `request(_ newDemand: Subscribers.Demand)` function getting called after this
|
||||
/// function returns
|
||||
///
|
||||
/// Later in the thread it's mentioned that as of `iOS 13.3` this behaviour changed to be synchronous but as of writing the minimum
|
||||
/// deployment version is set to `iOS 13.0` which I assume is why we are seeing the async behaviour which results in `receiveValue`
|
||||
/// not being called in some cases
|
||||
///
|
||||
/// When the project is eventually updated to have a minimum version higher than `iOS 13.3` we should re-test this behaviour to see if
|
||||
/// we can revert this change
|
||||
///
|
||||
/// https://forums.swift.org/t/combine-receive-on-runloop-main-loses-sent-value-how-can-i-make-it-work/28631/20
|
||||
let subscription: ReplaySubjectSubscription = ReplaySubjectSubscription<Output, Failure>(downstream: AnySubscriber(subscriber)) { [buffer = buffer, completion = completion] subscription in
|
||||
subscription.replay(buffer, completion: completion)
|
||||
}
|
||||
subscriber.receive(subscription: subscription)
|
||||
subscriptions.append(subscription)
|
||||
subscription.replay(buffer, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,17 +75,21 @@ public final class ReplaySubjectSubscription<Output, Failure: Error>: Subscripti
|
|||
private let downstream: AnySubscriber<Output, Failure>
|
||||
private var isCompleted: Bool = false
|
||||
private var demand: Subscribers.Demand = .none
|
||||
private var onInitialDemand: ((ReplaySubjectSubscription) -> ())?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(downstream: AnySubscriber<Output, Failure>) {
|
||||
init(downstream: AnySubscriber<Output, Failure>, onInitialDemand: @escaping (ReplaySubjectSubscription) -> ()) {
|
||||
self.downstream = downstream
|
||||
self.onInitialDemand = onInitialDemand
|
||||
}
|
||||
|
||||
// MARK: - Subscription
|
||||
|
||||
public func request(_ newDemand: Subscribers.Demand) {
|
||||
demand += newDemand
|
||||
onInitialDemand?(self)
|
||||
onInitialDemand = nil
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
|
|
|
@ -516,24 +516,6 @@ open class Storage {
|
|||
|
||||
// MARK: - Combine Extensions
|
||||
|
||||
public extension Storage {
|
||||
func readPublisherFlatMap<T>(
|
||||
value: @escaping (Database) throws -> AnyPublisher<T, Error>
|
||||
) -> AnyPublisher<T, Error> {
|
||||
return readPublisher(value: value)
|
||||
.flatMap { resultPublisher -> AnyPublisher<T, Error> in resultPublisher }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func writePublisherFlatMap<T>(
|
||||
updates: @escaping (Database) throws -> AnyPublisher<T, Error>
|
||||
) -> AnyPublisher<T, Error> {
|
||||
return writePublisher(updates: updates)
|
||||
.flatMap { resultPublisher -> AnyPublisher<T, Error> in resultPublisher }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
public extension ValueObservation {
|
||||
func publisher(
|
||||
in storage: Storage,
|
||||
|
|
|
@ -4,6 +4,10 @@ import Foundation
|
|||
import GRDB
|
||||
|
||||
open class Dependencies {
|
||||
/// These should not be accessed directly but rather via an instance of this type
|
||||
private static let _generalCacheInstance: MutableGeneralCacheType = General.Cache()
|
||||
private static let _generalCacheInstanceAccessQueue = DispatchQueue(label: "GeneralCacheInstanceAccess")
|
||||
|
||||
public var _subscribeQueue: Atomic<DispatchQueue?>
|
||||
public var subscribeQueue: DispatchQueue {
|
||||
get { Dependencies.getValueSettingIfNull(&_subscribeQueue) { DispatchQueue.global(qos: .default) } }
|
||||
|
@ -16,10 +20,25 @@ open class Dependencies {
|
|||
set { _receiveQueue.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
public var _generalCache: Atomic<Atomic<GeneralCacheType>?>
|
||||
public var generalCache: Atomic<GeneralCacheType> {
|
||||
get { Dependencies.getValueSettingIfNull(&_generalCache) { General.cache } }
|
||||
set { _generalCache.mutate { $0 = newValue } }
|
||||
public var _mutableGeneralCache: Atomic<MutableGeneralCacheType?>
|
||||
public var mutableGeneralCache: Atomic<MutableGeneralCacheType> {
|
||||
get {
|
||||
Dependencies.getMutableValueSettingIfNull(&_mutableGeneralCache) {
|
||||
Dependencies._generalCacheInstanceAccessQueue.sync { Dependencies._generalCacheInstance }
|
||||
}
|
||||
}
|
||||
}
|
||||
public var generalCache: GeneralCacheType {
|
||||
get {
|
||||
Dependencies.getValueSettingIfNull(&_mutableGeneralCache) {
|
||||
Dependencies._generalCacheInstanceAccessQueue.sync { Dependencies._generalCacheInstance }
|
||||
}
|
||||
}
|
||||
set {
|
||||
guard let mutableValue: MutableGeneralCacheType = newValue as? MutableGeneralCacheType else { return }
|
||||
|
||||
_mutableGeneralCache.mutate { $0 = mutableValue }
|
||||
}
|
||||
}
|
||||
|
||||
public var _storage: Atomic<Storage?>
|
||||
|
@ -51,7 +70,7 @@ open class Dependencies {
|
|||
public init(
|
||||
subscribeQueue: DispatchQueue? = nil,
|
||||
receiveQueue: DispatchQueue? = nil,
|
||||
generalCache: Atomic<GeneralCacheType>? = nil,
|
||||
generalCache: MutableGeneralCacheType? = nil,
|
||||
storage: Storage? = nil,
|
||||
scheduler: ValueObservationScheduler? = nil,
|
||||
standardUserDefaults: UserDefaultsType? = nil,
|
||||
|
@ -59,7 +78,7 @@ open class Dependencies {
|
|||
) {
|
||||
_subscribeQueue = Atomic(subscribeQueue)
|
||||
_receiveQueue = Atomic(receiveQueue)
|
||||
_generalCache = Atomic(generalCache)
|
||||
_mutableGeneralCache = Atomic(generalCache)
|
||||
_storage = Atomic(storage)
|
||||
_scheduler = Atomic(scheduler)
|
||||
_standardUserDefaults = Atomic(standardUserDefaults)
|
||||
|
@ -77,4 +96,14 @@ open class Dependencies {
|
|||
|
||||
return value
|
||||
}
|
||||
|
||||
public static func getMutableValueSettingIfNull<T>(_ maybeValue: inout Atomic<T?>, _ valueGenerator: () -> T) -> Atomic<T> {
|
||||
guard let value: T = maybeValue.wrappedValue else {
|
||||
let value: T = valueGenerator()
|
||||
maybeValue.mutate { $0 = value }
|
||||
return Atomic(value)
|
||||
}
|
||||
|
||||
return Atomic(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,35 +3,48 @@
|
|||
import Foundation
|
||||
import GRDB
|
||||
|
||||
public protocol GeneralCacheType {
|
||||
var encodedPublicKey: String? { get set }
|
||||
var recentReactionTimestamps: [Int64] { get set }
|
||||
}
|
||||
// MARK: - General.Cache
|
||||
|
||||
public enum General {
|
||||
public class Cache: GeneralCacheType {
|
||||
public class Cache: MutableGeneralCacheType {
|
||||
public var encodedPublicKey: String? = nil
|
||||
public var recentReactionTimestamps: [Int64] = []
|
||||
}
|
||||
|
||||
public static var cache: Atomic<GeneralCacheType> = Atomic(Cache())
|
||||
}
|
||||
|
||||
// MARK: - GeneralError
|
||||
|
||||
public enum GeneralError: Error {
|
||||
case invalidSeed
|
||||
case keyGenerationFailed
|
||||
case randomGenerationFailed
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public func getUserHexEncodedPublicKey(_ db: Database? = nil, dependencies: Dependencies = Dependencies()) -> String {
|
||||
if let cachedKey: String = dependencies.generalCache.wrappedValue.encodedPublicKey { return cachedKey }
|
||||
if let cachedKey: String = dependencies.generalCache.encodedPublicKey { return cachedKey }
|
||||
|
||||
if let publicKey: Data = Identity.fetchUserPublicKey(db) { // Can be nil under some circumstances
|
||||
let sessionId: SessionId = SessionId(.standard, publicKey: publicKey.bytes)
|
||||
|
||||
dependencies.generalCache.mutate { $0.encodedPublicKey = sessionId.hexString }
|
||||
dependencies.mutableGeneralCache.mutate { $0.encodedPublicKey = sessionId.hexString }
|
||||
return sessionId.hexString
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// MARK: - GeneralCacheType
|
||||
|
||||
public protocol MutableGeneralCacheType: GeneralCacheType {
|
||||
var encodedPublicKey: String? { get set }
|
||||
var recentReactionTimestamps: [Int64] { get set }
|
||||
}
|
||||
|
||||
/// This is a read-only version of the `OGMMutableCacheType` designed to avoid unintentionally mutating the instance in a
|
||||
/// non-thread-safe way
|
||||
public protocol GeneralCacheType {
|
||||
var encodedPublicKey: String? { get }
|
||||
var recentReactionTimestamps: [Int64] { get }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue