Files
bundesmessenger-ios/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m
T
ismailgulek c64993ad1d Release 1.9.12 (#7081)
* Update voice broadcast tiles UI (#6965)

* Translated using Weblate (German)

Currently translated at 100.0% (2307 of 2307 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* speeding the animation a bit

* tests and identifier improvements

* fix

* changelog

* removed unused code

* Avoid unnecessary send state request (#6970)

* comment

* Curate MXCrypto protocol methods

* Add voice broadcast initial state in bubble data (#6972)

- Add voice broadcast initial state in bubble data
- Remove the local record after sending

* Voice Broadcast: log and block unexpected state change

* Sing out bottom bar

* new line

* Enable WYSIWYG plain text support

* Remove change on Apple swift-collections revision

* removed RiotSettings a non RiotSwiftUI reference from the ViewState code

* fixed a test

* Complete MXCryptoV2 implementation

* Multi session logut

* Switch the CI to code 14 and the iOS 14 simulator, fix UI tests

* Fixes #6987 - Prevent ZXing from unnecessarily requesting camera access

* Fixes #6988 - Prevent actor switching when tearing down the rendezvous

* Separator fix

* Removed warnings

* add Z-Labs tag or rich text editor and update to the new label naming

* changelog

* Hide old sessions list when the new dm is enabled

* Add changelog.d file

* Sing out filtering

* Avoid simultaneous state changes (#6986)

* Improve kebab menu in UserSessionOverview

* Add UI tests

* Add changelog.d file

* No customization for emptycell (#7000)

* PSG-976 Exclude current session from security recommendations and other sessions

* Padding fix

* Fixed unit tests

* Add empty onLearnMoreAction closure

* Add InfoView skeleton

* Add UserSessionOverviewViewBindings

* Style info view

* Add bottom sheet modifier

* Localise content

* Add inactive sessions copy

* Fix bug in InlineTextButton

* Improve UserSessionCardView

* Add “learn more” button in UserOtherSessions

* Show bottom sheet in user other sessions

* Show rename info alert

* Refine UX

* Add iOS 15- fallback

* Refine InfoView

* Add UI tests

* Improve UserOtherSessionsUITests

* Improve InlineTextButton API

* Add changelod.d file

* Fix failing UTs

* Hide keyboard in UserSessionName

* Add .viewSessionInfo view action

* Voice Broadcast - BugFix - send the last chunk (#7002)

* Voice Broadcast - BugFix - send the last chunk with the right sequence number

- we reset now and teardown the service only after the last chunk is sent

* updated package + tests

* change log

* Bug Fix : Crash if the room has avatar and voice broadcast tiles

* Add MVVM-C for InfoSheet

* improving UI tests for slow CI

* removing comment

* test improvements for slow ci

* Show bottom sheet in other sessions screen

* Show bottom sheet in rename session screen

* Delete bottom sheet modifier

* Show rename sheet

* UI and unit tests

* Refresh fix

* Changelog

* Add InfoSheet SwiftUI preview

* simplify the test to make it pass on the CI

* Fix memory leak

* Cleanup UI tests

* improving tests for the CI

* Fixed IRC-style message and commands support in Rich text editor

* tests updated for the CI

* test improvements

* removing a test that can't pass on the CI due to its speed

* Changelog

* CryptoV2 changes

* Display crypto version

* Voice broadcast - Disable the sleep mode during the recording until we are able to handle it

Currently go to "sleep mode" pauses the voice broadcast recording

* Add issue automation for the VoIP team

* Renamed sign out to logout

* Renamed sign out to logout

* Renamed sign out to logout

* Sign out of all other sessions

* Fix typo in issue automation

* Fixed unit tests

* Translations update from Weblate (#7017)

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (German)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (German)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com>
Co-authored-by: Priit Jõerüüt <riot@joeruut.com>
Co-authored-by: Vri <element@vrifox.cc>
Co-authored-by: Johan Smits <johan@smitsmail.net>

* Prepare for new sprint

* Prepare for new sprint

* Threads: added support to read receipts (MSC3771)

- Update after review

* Threads: added support to notifications count (MSC3773)

* Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift

Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com>

* Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift

Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com>

* Comment fix

* the test may fail on CI without blocking the task/check

* tests may fail on CI

* test improvement

* test may fail on CI

* Hide push toggles for http pushers when there is no server support

* changelog

* Code review fixes

* Threads: added support to read receipts (MSC3771)

- Update after review

* Synchronise composer and toolbar resizing animation duration

* Add kResizeComposerAnimationDuration constant description

* fix for 6946

* Threads: add support to labs flag for read receipts

* Cleanup

* Code review fixes, created DestructiveButton

* Update issue automation

Stop using deprecated ProjectNext API in favour of the new ProjectV2 one

* Update PR automation

Stop using deprecated ProjectNext API in favour of the new ProjectV2 one

* Code review fixes

* Map location info

* Map location info

* Add location feature in UserSessionsOverview

* Add “show location” feature in other sessions list

* Add “show location“ feature in session overview

* Fix Package.resolved

* Cleanup merge leftovers

* Cleanup code

* Cleanup

* Add show/hide ip persistency

* Add location info in UserOtherSessions

* Refine settings logic

* Mock settings in UserSessionsOverviewViewModel

* Add settings service in UserOtherSessionsViewModel

* Inject setting service in UserSessionOverviewViewModel

* Add changelog.d file

* Fix UTs

* Cleanup merge leftovers

* Add animations

* Fix failing test

* Amend title font

* Amend copies

* Device Manager: Session list item is not tappable everywhere

* changelog

* Threads notification count in main timeline including un participated threads

* Changed title and body

* Removed "Do not ask again" button

* Remove indication about plain text mode coming soon

* Prevent `Unable to activate constraint with anchors .. because they have no common ancestor.` crashes. Only link toasts to the top safe area instead of the navigation controller

* Revert "Replace attributed string height calculation with a more reliable implementation"

This reverts commit 47512127ec.

* Revert "Fix timeline items text height calculation"

This reverts commit 27f4feb8d9.

* Revert "Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation (#6679)"

This reverts commit bd017d1a77.

* Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation

* Prepare for new sprint

* Refine bottom sheet layout

* updated pod

* changelog

* Switch to using an API key for interactions with AppStoreConnect while on CI; update fastlane and dependencies

* Rich-text editor: Fix text formatting enabled inconsistent state

* Labs: Rich-text editor - Fix text formatting switch losing the current content of the composer

* Re-order View computed properties and move to private mark

* Add intrinsic sized bottom sheet

* Snooze controller

* Changelog

* Fix composer view model tests

* Rich-text editor: enable translations between Markdown and HTML when toggling text formatting

* Force a layout on the room bubble cell messageTextView to get a correct frame

* Move Move UserAgentParserTests

* Add UserSessionDetailsUITests

* Improve UserSessionNameUITests

* Cleanup tests

* Improve UserSessionNameViewModelTests

* Test empty state for UserOtherSessions

* Fix typo

* Cleanup unused code

* Add changelog.d file

* Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773

* Remove 10s wait on failed initial sync

* Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773

- Update after review

* Revert "Device Manager: Session list item is not tappable everywhere"

This reverts commit 221f4ad106.

* Fixup session list item is not tappable everywhere

* Fix accessibility id in UserOtherSessions

* Poll not usable after logging out and back in

* Changelog

* Removed init

* voice dictation now works

* plain text

* Add voice broadcast slider (#7010)

* Display number of unread messages above threads button

* Translations update from Weblate (#7080)

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

* Translated using Weblate (German)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

* Translated using Weblate (German)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Albanian)

Currently translated at 99.6% (2303 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/

* Translated using Weblate (German)

Currently translated at 100.0% (2313 of 2313 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2313 of 2313 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2313 of 2313 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

* Translated using Weblate (German)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Albanian)

Currently translated at 99.5% (2305 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/

* Translated using Weblate (German)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Japanese)

Currently translated at 66.2% (1534 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Japanese)

Currently translated at 66.2% (1534 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/

* Translated using Weblate (German)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (German)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (French)

Currently translated at 97.6% (2272 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/

* Translated using Weblate (Russian)

Currently translated at 80.5% (1873 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/

* Translated using Weblate (Russian)

Currently translated at 80.7% (1879 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/

Co-authored-by: Roel ter Maat <roel.termaat@nedap.com>
Co-authored-by: Vri <element@vrifox.cc>
Co-authored-by: Besnik Bleta <besnik@programeshqip.org>
Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Priit Jõerüüt <riot@joeruut.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: random <dictionary@tutamail.com>
Co-authored-by: Szimszon <github@oregpreshaz.eu>
Co-authored-by: Suguru Hirahara <ovestekona@protonmail.com>
Co-authored-by: Thibault Martin <mail@thibaultmart.in>
Co-authored-by: Platon Terekhov <ockenfels_vevent@aleeas.com>

* changelog.d: Upgrade MatrixSDK version ([v0.24.3](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.3)).

* version++

Co-authored-by: giomfo <gforet@matrix.org>
Co-authored-by: Yoan Pintas <y.pintas@gmail.com>
Co-authored-by: Vri <element@vrifox.cc>
Co-authored-by: Anderas <andyuhnak@gmail.com>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>
Co-authored-by: Giom Foret <giom@matrix.org>
Co-authored-by: Aleksandrs Proskurins <paleksandrs@gmail.com>
Co-authored-by: aringenbach <arnaudr@element.io>
Co-authored-by: manuroe <manuroe@users.noreply.github.com>
Co-authored-by: David Langley <langley.dave@gmail.com>
Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com>
Co-authored-by: Alfonso Grillo <alfogrillo@element.io>
Co-authored-by: Kat Gerasimova <ekaterinag@element.io>
Co-authored-by: Element Translate Bot <admin@riot.im>
Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com>
Co-authored-by: Priit Jõerüüt <riot@joeruut.com>
Co-authored-by: Johan Smits <johan@smitsmail.net>
Co-authored-by: gulekismail <ismailgulek0@gmail.com>
Co-authored-by: Gil Eluard <gile@element.io>
Co-authored-by: Aleksandrs Proskurins <aleksandrsp@element.io>
Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com>
Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
Co-authored-by: Roel ter Maat <roel.termaat@nedap.com>
Co-authored-by: Besnik Bleta <besnik@programeshqip.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: random <dictionary@tutamail.com>
Co-authored-by: Szimszon <github@oregpreshaz.eu>
Co-authored-by: Suguru Hirahara <ovestekona@protonmail.com>
Co-authored-by: Thibault Martin <mail@thibaultmart.in>
Co-authored-by: Platon Terekhov <ockenfels_vevent@aleeas.com>
2022-11-15 16:40:36 +03:00

2352 lines
82 KiB
Objective-C

/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKAccount.h"
#import "MXKAccountManager.h"
#import "MXKRoomDataSourceManager.h"
#import "MXKEventFormatter.h"
#import "MXKTools.h"
#import "MXKContactManager.h"
#import "MXKConstants.h"
#import "NSBundle+MatrixKit.h"
#import <AFNetworking/AFNetworking.h>
#import <MatrixSDK/MXBackgroundModeHandler.h>
#import "MXKSwiftHeader.h"
#import "GeneratedInterface-Swift.h"
NSString *const kMXKAccountUserInfoDidChangeNotification = @"kMXKAccountUserInfoDidChangeNotification";
NSString *const kMXKAccountAPNSActivityDidChangeNotification = @"kMXKAccountAPNSActivityDidChangeNotification";
NSString *const kMXKAccountPushKitActivityDidChangeNotification = @"kMXKAccountPushKitActivityDidChangeNotification";
NSString *const kMXKAccountErrorDomain = @"kMXKAccountErrorDomain";
static MXKAccountOnCertificateChange _onCertificateChangeBlock;
/**
HTTP status codes for error cases on initial sync requests, for which errors will not be propagated to the client.
*/
static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
@interface MXKAccount ()
{
// We will notify user only once on session failure
BOOL notifyOpenSessionFailure;
// Reachability observer
id reachabilityObserver;
// Session state observer
id sessionStateObserver;
// Handle user's settings change
id userUpdateListener;
// Used for logging application start up
NSDate *openSessionStartDate;
// Event notifications listener
id notificationCenterListener;
// Internal list of ignored rooms
NSMutableArray* ignoredRooms;
// Background sync management
MXOnBackgroundSyncDone backgroundSyncDone;
MXOnBackgroundSyncFail backgroundSyncFails;
NSTimer* backgroundSyncTimer;
// Observe UIApplicationSignificantTimeChangeNotification to refresh MXRoomSummaries on time formatting change.
id UIApplicationSignificantTimeChangeNotificationObserver;
// Observe NSCurrentLocaleDidChangeNotification to refresh MXRoomSummaries on time formatting change.
id NSCurrentLocaleDidChangeNotificationObserver;
MXPusher *currentPusher;
}
/// Will be true if the session is not in a pauseable state or we requested for the session to pause but not finished yet. Will be reverted to false again after `resume` called.
@property (nonatomic, assign, getter=isPauseRequested) BOOL pauseRequested;
@property (nonatomic, strong) id<MXBackgroundTask> pauseBackgroundTask;
@property (nonatomic, strong) id<MXBackgroundTask> backgroundSyncBgTask;
@end
@implementation MXKAccount
@synthesize mxSession, mxRestClient;
@synthesize userPresence;
@synthesize userTintColor;
@synthesize hideUserPresence;
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initialSyncSilentErrorsHTTPStatusCodes = @[
@(504),
@(522),
@(524),
@(599)
];
});
}
+ (void)registerOnCertificateChangeBlock:(MXKAccountOnCertificateChange)onCertificateChangeBlock
{
_onCertificateChangeBlock = onCertificateChangeBlock;
}
+ (UIColor*)presenceColor:(MXPresence)presence
{
switch (presence)
{
case MXPresenceOnline:
return [[MXKAppSettings standardAppSettings] presenceColorForOnlineUser];
case MXPresenceUnavailable:
return [[MXKAppSettings standardAppSettings] presenceColorForUnavailableUser];
case MXPresenceOffline:
return [[MXKAppSettings standardAppSettings] presenceColorForOfflineUser];
case MXPresenceUnknown:
default:
return nil;
}
}
- (nonnull instancetype)initWithCredentials:(MXCredentials*)credentials
{
if (self = [super init])
{
notifyOpenSessionFailure = YES;
// Report credentials and alloc REST client.
_mxCredentials = credentials;
[self prepareRESTClient];
userPresence = MXPresenceUnknown;
// Refresh device information
[self loadDeviceInformation:nil failure:nil];
[self loadCurrentPusher:nil failure:nil];
[self registerAccountDataDidChangeIdentityServerNotification];
[self registerIdentityServiceDidChangeAccessTokenNotification];
}
return self;
}
- (void)dealloc
{
[self closeSession:NO];
mxSession = nil;
[mxRestClient close];
mxRestClient = nil;
}
#pragma mark - NSCoding
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
notifyOpenSessionFailure = YES;
[self prepareRESTClient];
[self registerAccountDataDidChangeIdentityServerNotification];
[self registerIdentityServiceDidChangeAccessTokenNotification];
userPresence = MXPresenceUnknown;
// Refresh device information
[self loadDeviceInformation:nil failure:nil];
[self loadCurrentPusher:nil failure:nil];
}
return self;
}
#pragma mark - Properties
- (void)setIdentityServerURL:(NSString *)identityServerURL
{
if (identityServerURL.length)
{
_identityServerURL = identityServerURL;
self.mxCredentials.identityServer = identityServerURL;
// Update services used in MXSession
[mxSession setIdentityServer:self.mxCredentials.identityServer andAccessToken:self.mxCredentials.identityServerAccessToken];
}
else
{
_identityServerURL = nil;
[mxSession setIdentityServer:nil andAccessToken:nil];
}
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (void)setAntivirusServerURL:(NSString *)antivirusServerURL
{
_antivirusServerURL = antivirusServerURL;
// Update the current session if any
[mxSession setAntivirusServerURL:antivirusServerURL];
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (void)setPushGatewayURL:(NSString *)pushGatewayURL
{
_pushGatewayURL = pushGatewayURL.length ? pushGatewayURL : nil;
MXLogDebug(@"[MXKAccount][Push] setPushGatewayURL: %@", _pushGatewayURL);
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (NSString*)userDisplayName
{
if (mxSession)
{
return mxSession.myUser.displayname;
}
return nil;
}
- (NSString*)userAvatarUrl
{
if (mxSession)
{
return mxSession.myUser.avatarUrl;
}
return nil;
}
- (NSString*)fullDisplayName
{
if (self.userDisplayName.length)
{
return [NSString stringWithFormat:@"%@ (%@)", self.userDisplayName, self.mxCredentials.userId];
}
else
{
return self.mxCredentials.userId;
}
}
- (NSArray<NSString *> *)linkedEmails
{
NSMutableArray<NSString *> *linkedEmails = [NSMutableArray array];
for (MXThirdPartyIdentifier *threePID in self.threePIDs)
{
if ([threePID.medium isEqualToString:kMX3PIDMediumEmail])
{
[linkedEmails addObject:threePID.address];
}
}
return linkedEmails;
}
- (NSArray<NSString *> *)linkedPhoneNumbers
{
NSMutableArray<NSString *> *linkedPhoneNumbers = [NSMutableArray array];
for (MXThirdPartyIdentifier *threePID in self.threePIDs)
{
if ([threePID.medium isEqualToString:kMX3PIDMediumMSISDN])
{
[linkedPhoneNumbers addObject:threePID.address];
}
}
return linkedPhoneNumbers;
}
- (UIColor*)userTintColor
{
if (!userTintColor)
{
userTintColor = [MXKTools colorWithRGBValue:[self.mxCredentials.userId hash]];
}
return userTintColor;
}
- (BOOL)pushNotificationServiceIsActive
{
if (currentPusher && currentPusher.enabled)
{
MXLogDebug(@"[MXKAccount][Push] pushNotificationServiceIsActive: currentPusher.enabled %@", currentPusher.enabled);
return currentPusher.enabled.boolValue;
}
BOOL pushNotificationServiceIsActive = ([[MXKAccountManager sharedManager] isAPNSAvailable] && self.hasPusherForPushNotifications && mxSession);
MXLogDebug(@"[MXKAccount][Push] pushNotificationServiceIsActive: %@", @(pushNotificationServiceIsActive));
return pushNotificationServiceIsActive;
}
- (void)enablePushNotifications:(BOOL)enable
success:(void (^)(void))success
failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: %@", @(enable));
if (enable)
{
if (currentPusher && currentPusher.enabled && !currentPusher.enabled.boolValue)
{
[self.mxSession.matrixRestClient setPusherWithPushkey:currentPusher.pushkey
kind:currentPusher.kind
appId:currentPusher.appId
appDisplayName:currentPusher.appDisplayName
deviceDisplayName:currentPusher.deviceDisplayName
profileTag:currentPusher.profileTag
lang:currentPusher.lang
data:currentPusher.data.JSONDictionary
append:NO
enabled:enable
success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: remotely enabled Push: Success");
[self loadCurrentPusher:^{
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogWarning(@"[MXKAccount][Push] enablePushNotifications: load current pusher failed with error: %@", error);
if (failure)
{
failure(error);
}
}];
} failure:^(NSError *error) {
MXLogWarning(@"[MXKAccount][Push] enablePushNotifications: remotely enable push failed with error: %@", error);
if (failure)
{
failure(error);
}
}];
}
else if ([[MXKAccountManager sharedManager] isAPNSAvailable])
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Enable Push for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enableAPNSPusher:YES success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Enable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Enable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Error: Cannot enable Push");
NSError *error = [NSError errorWithDomain:kMXKAccountErrorDomain
code:0
userInfo:@{
NSLocalizedDescriptionKey:
[VectorL10n accountErrorPushNotAllowed]
}];
if (failure)
{
failure (error);
}
}
}
else if (self.hasPusherForPushNotifications || currentPusher)
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Disable APNS for %@ account", self.mxCredentials.userId);
// Delete the pusher, report the new value only on success.
[self enableAPNSPusher:NO
success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Disable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Disable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
}
- (BOOL)isPushKitNotificationActive
{
BOOL isPushKitNotificationActive = ([[MXKAccountManager sharedManager] isPushAvailable] && self.hasPusherForPushKitNotifications && mxSession);
MXLogDebug(@"[MXKAccount][Push] isPushKitNotificationActive: %@", @(isPushKitNotificationActive));
return isPushKitNotificationActive;
}
- (void)enablePushKitNotifications:(BOOL)enable
success:(void (^)(void))success
failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: %@", @(enable));
if (enable)
{
if ([[MXKAccountManager sharedManager] isPushAvailable])
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Enable Push for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enablePushKitPusher:YES success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Enable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Enable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Error: Cannot enable Push");
NSError *error = [NSError errorWithDomain:kMXKAccountErrorDomain
code:0
userInfo:@{
NSLocalizedDescriptionKey:
[VectorL10n accountErrorPushNotAllowed]
}];
failure (error);
}
}
else if (self.hasPusherForPushKitNotifications)
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Disable Push for %@ account", self.mxCredentials.userId);
// Delete the pusher, report the new value only on success.
[self enablePushKitPusher:NO success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Disable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Disable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: PushKit is already disabled for %@", self.mxCredentials.userId);
if (success)
{
success();
}
}
}
- (void)setEnableInAppNotifications:(BOOL)enableInAppNotifications
{
MXLogDebug(@"[MXKAccount] setEnableInAppNotifications: %@", @(enableInAppNotifications));
_enableInAppNotifications = enableInAppNotifications;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (void)setDisabled:(BOOL)disabled
{
if (_disabled != disabled)
{
_disabled = disabled;
if (_disabled)
{
[self deletePusher];
[self enablePushKitNotifications:NO success:nil failure:nil];
// Close session (keep the storage).
[self closeSession:NO];
}
else if (!mxSession)
{
// Open a new matrix session
id<MXStore> store = [[[MXKAccountManager sharedManager].storeClass alloc] init];
[self openSessionWithStore:store];
}
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
}
- (void)setWarnedAboutEncryption:(BOOL)warnedAboutEncryption
{
_warnedAboutEncryption = warnedAboutEncryption;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (NSMutableDictionary<NSString *, id<NSCoding>> *)others
{
if(_others == nil)
{
_others = [NSMutableDictionary dictionary];
}
return _others;
}
- (void)setPauseRequested:(BOOL)pauseRequested
{
if (_pauseRequested != pauseRequested)
{
_pauseRequested = pauseRequested;
if (_pauseRequested)
{
// Make sure the SDK finish its work before the app goes sleeping in background
id<MXBackgroundModeHandler> handler = [MXSDKOptions sharedInstance].backgroundModeHandler;
if (handler)
{
if (!self.pauseBackgroundTask.isRunning)
{
self.pauseBackgroundTask = [handler startBackgroundTaskWithName:@"[MXKAccount] pauseInBackgroundTask"
expirationHandler:nil];
}
}
}
else
{
[self cancelPauseBackgroundTask];
}
}
}
#pragma mark - Matrix user's profile
- (void)setUserDisplayName:(NSString*)displayname success:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (mxSession && mxSession.myUser)
{
[mxSession.myUser setDisplayName:displayname
success:^{
if (success) {
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
failure:failure];
}
else if (failure)
{
failure ([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey: [VectorL10n accountErrorMatrixSessionIsNotOpened]}]);
}
}
- (void)setUserAvatarUrl:(NSString*)avatarUrl success:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (mxSession && mxSession.myUser)
{
[mxSession.myUser setAvatarUrl:avatarUrl
success:^{
if (success) {
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
failure:failure];
}
else if (failure)
{
failure ([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey: [VectorL10n accountErrorMatrixSessionIsNotOpened]}]);
}
}
- (void)changePassword:(NSString*)oldPassword with:(NSString*)newPassword logoutDevices:(BOOL)logoutDevices success:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (mxSession)
{
[mxRestClient changePassword:oldPassword
with:newPassword
logoutDevices:logoutDevices
success:^{
if (success) {
success();
}
}
failure:failure];
}
else if (failure)
{
failure ([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey: [VectorL10n accountErrorMatrixSessionIsNotOpened]}]);
}
}
- (void)load3PIDs:(void (^)(void))success failure:(void (^)(NSError *))failure
{
[mxRestClient threePIDs:^(NSArray<MXThirdPartyIdentifier *> *threePIDs2) {
self->_threePIDs = threePIDs2;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
if (success)
{
success();
}
} failure:^(NSError *error) {
if (failure)
{
failure(error);
}
}];
}
- (void)loadCurrentPusher:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (!self.mxSession.myDeviceId)
{
MXLogWarning(@"[MXKAccount] loadPusher: device ID not found");
if (failure)
{
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
return;
}
[self.mxSession supportedMatrixVersions:^(MXMatrixVersions *matrixVersions) {
if (!matrixVersions.supportsRemotelyTogglingPushNotifications)
{
MXLogDebug(@"[MXKAccount] loadPusher: remotely toggling push notifications not supported");
if (success)
{
success();
}
return;
}
[self.mxSession.matrixRestClient pushers:^(NSArray<MXPusher *> *pushers) {
MXPusher *ownPusher;
for (MXPusher *pusher in pushers)
{
if ([pusher.deviceId isEqualToString:self.mxSession.myDeviceId])
{
ownPusher = pusher;
}
}
self->currentPusher = ownPusher;
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogWarning(@"[MXKAccount] loadPusher: get pushers failed due to error %@", error);
if (failure)
{
failure(error);
}
}];
} failure:^(NSError *error) {
MXLogWarning(@"[MXKAccount] loadPusher: supportedMatrixVersions failed due to error %@", error);
if (failure)
{
failure(error);
}
}];
}
- (void)loadDeviceInformation:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (self.mxCredentials.deviceId)
{
[mxRestClient deviceByDeviceId:self.mxCredentials.deviceId success:^(MXDevice *device) {
self->_device = device;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
if (success)
{
success();
}
} failure:^(NSError *error) {
if (failure)
{
failure(error);
}
}];
}
else
{
_device = nil;
if (success)
{
success();
}
}
}
- (void)setUserPresence:(MXPresence)presence andStatusMessage:(NSString *)statusMessage completion:(void (^)(void))completion
{
userPresence = presence;
if (mxSession && !hideUserPresence)
{
// Update user presence on server side
[mxSession.myUser setPresence:userPresence
andStatusMessage:statusMessage
success:^{
MXLogDebug(@"[MXKAccount] %@: set user presence (%lu) succeeded", self.mxCredentials.userId, (unsigned long)self->userPresence);
if (completion)
{
completion();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount] %@: set user presence (%lu) failed", self.mxCredentials.userId, (unsigned long)self->userPresence);
}];
}
else if (hideUserPresence)
{
MXLogDebug(@"[MXKAccount] %@: set user presence is disabled.", self.mxCredentials.userId);
}
}
#pragma mark -
/**
Create a matrix session based on the provided store.
When store data is ready, the live stream is automatically launched by synchronising the session with the server.
In case of failure during server sync, the method is reiterated until the data is up-to-date with the server.
This loop is stopped if you call [MXCAccount closeSession:], it is suspended if you call [MXCAccount pauseInBackgroundTask].
@param store the store to use for the session.
*/
-(void)openSessionWithStore:(id<MXStore>)store
{
// Sanity check
if (!self.mxCredentials || !mxRestClient)
{
MXLogDebug(@"[MXKAccount] Matrix session cannot be created without credentials");
return;
}
// Close potential session (keep associated store).
[self closeSession:NO];
openSessionStartDate = [NSDate date];
// Instantiate new session
mxSession = [[MXSession alloc] initWithMatrixRestClient:mxRestClient];
mxSession.preferredSyncPresence = self.preferredSyncPresence;
// Check whether an antivirus url is defined.
if (_antivirusServerURL)
{
// Enable the antivirus scanner in the current session.
[mxSession setAntivirusServerURL:_antivirusServerURL];
}
// Set default MXEvent -> NSString formatter
MXKEventFormatter *eventFormatter = [[MXKEventFormatter alloc] initWithMatrixSession:self.mxSession];
eventFormatter.isForSubtitle = YES;
// Apply the event types filter to display only the wanted event types.
eventFormatter.eventTypesFilterForMessages = [MXKAppSettings standardAppSettings].eventsFilterForMessages;
mxSession.roomSummaryUpdateDelegate = eventFormatter;
// Observe UIApplicationSignificantTimeChangeNotification to refresh to MXRoomSummaries if date/time are shown.
// UIApplicationSignificantTimeChangeNotification is posted if DST is updated, carrier time is updated
UIApplicationSignificantTimeChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationSignificantTimeChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
[self onDateTimeFormatUpdate];
}];
// Observe NSCurrentLocaleDidChangeNotification to refresh MXRoomSummaries if date/time are shown.
// NSCurrentLocaleDidChangeNotification is triggered when the time swicthes to AM/PM to 24h time format
NSCurrentLocaleDidChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSCurrentLocaleDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
[self onDateTimeFormatUpdate];
}];
// Force a date refresh for all the last messages.
[self onDateTimeFormatUpdate];
// Register session state observer
sessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
// Check whether the concerned session is the associated one
if (notif.object == self->mxSession)
{
[self onMatrixSessionStateChange];
}
}];
MXWeakify(self);
[mxSession setStore:store success:^{
// Complete session registration by launching live stream
MXStrongifyAndReturnIfNil(self);
// Validate the availability of local contact sync for any changes to the
// authorization of contacts access that may have occurred since the last launch.
// The session is passed in as the contacts manager may not have had a session added yet.
[MXKContactManager.sharedManager validateSyncLocalContactsStateForSession:self.mxSession];
// Refresh pusher state
[self loadCurrentPusher:^{
[self refreshAPNSPusher];
} failure:nil];
[self refreshPushKitPusher];
// Launch server sync
[self launchInitialServerSync];
} failure:^(NSError *error) {
// This cannot happen. Loading of MXFileStore cannot fail.
MXStrongifyAndReturnIfNil(self);
self->mxSession = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self->sessionStateObserver];
self->sessionStateObserver = nil;
}];
}
/**
Close the matrix session.
@param clearStore set YES to delete all store data.
*/
- (void)closeSession:(BOOL)clearStore
{
MXLogDebug(@"[MXKAccount] closeSession (%u)", clearStore);
if (NSCurrentLocaleDidChangeNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:NSCurrentLocaleDidChangeNotificationObserver];
NSCurrentLocaleDidChangeNotificationObserver = nil;
}
if (UIApplicationSignificantTimeChangeNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:UIApplicationSignificantTimeChangeNotificationObserver];
UIApplicationSignificantTimeChangeNotificationObserver = nil;
}
[self removeNotificationListener];
if (reachabilityObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
reachabilityObserver = nil;
}
if (sessionStateObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:sessionStateObserver];
sessionStateObserver = nil;
}
if (userUpdateListener)
{
[mxSession.myUser removeListener:userUpdateListener];
userUpdateListener = nil;
}
if (mxSession)
{
// Reset room data stored in memory
[MXKRoomDataSourceManager removeSharedManagerForMatrixSession:mxSession];
if (clearStore)
{
// Force a reload of device keys at the next session start.
// This will fix potential UISIs other peoples receive for our messages.
if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]])
{
[(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys];
}
// Clean other stores
[mxSession.scanManager deleteAllAntivirusScans];
[mxSession.aggregations resetData];
}
else
{
// For recomputing of room summaries as they are a cache of computed data
[mxSession resetRoomsSummariesLastMessage];
}
// Close session
[mxSession close];
if (clearStore)
{
[mxSession.store deleteAllData];
}
mxSession = nil;
}
notifyOpenSessionFailure = YES;
}
- (void)logout:(void (^)(void))completion
{
if (!mxSession || !mxSession.matrixRestClient)
{
MXLogDebug(@"[MXKAccount] logout: Need to open the closed session to make a logout request");
id<MXStore> store = [[[MXKAccountManager sharedManager].storeClass alloc] init];
mxSession = [[MXSession alloc] initWithMatrixRestClient:mxRestClient];
MXWeakify(self);
[mxSession setStore:store success:^{
MXStrongifyAndReturnIfNil(self);
[self logout:completion];
} failure:^(NSError *error) {
completion();
}];
return;
}
[self deletePusher];
[self enablePushKitNotifications:NO success:nil failure:nil];
MXHTTPOperation *operation = [mxSession logout:^{
[self closeSession:YES];
if (completion)
{
completion();
}
} failure:^(NSError *error) {
// Close the session even if the logout request failed
[self closeSession:YES];
if (completion)
{
completion();
}
}];
// Do not retry on failure.
operation.maxNumberOfTries = 1;
}
// Logout locally, do not send server request
- (void)logoutLocally:(void (^)(void))completion
{
[self deletePusher];
[self enablePushKitNotifications:NO success:nil failure:nil];
[mxSession enableCrypto:NO success:^{
[self closeSession:YES];
if (completion)
{
completion();
}
} failure:^(NSError *error) {
// Close the session even if the logout request failed
[self closeSession:YES];
if (completion)
{
completion();
}
}];
}
- (void)logoutSendingServerRequest:(BOOL)sendLogoutServerRequest
completion:(void (^)(void))completion
{
if (sendLogoutServerRequest)
{
[self logout:completion];
}
else
{
[self logoutLocally:completion];
}
}
#pragma mark - Soft logout
- (void)softLogout
{
if (_isSoftLogout)
{
// do not close the session if already soft logged out
// it may break the current logout request and resetting session credentials can cause crashes
return;
}
_isSoftLogout = YES;
[[MXKAccountManager sharedManager] saveAccounts];
// Stop SDK making requests to the homeserver
[mxSession close];
}
- (void)hydrateWithCredentials:(MXCredentials*)credentials
{
// Sanity check
if ([self.mxCredentials.userId isEqualToString:credentials.userId])
{
_mxCredentials = credentials;
_isSoftLogout = NO;
[[MXKAccountManager sharedManager] saveAccounts];
[self prepareRESTClient];
}
else
{
MXLogDebug(@"[MXKAccount] hydrateWithCredentials: Error: users ids mismatch: %@ vs %@", credentials.userId, self.mxCredentials.userId);
}
}
- (void)deletePusher
{
if (self.pushNotificationServiceIsActive)
{
[self enableAPNSPusher:NO success:nil failure:nil];
}
}
- (void)pauseInBackgroundTask
{
if (mxSession == nil)
{
// no session to pause
return;
}
// mark that we want to pause when possible
self.pauseRequested = YES;
if (mxSession.isPauseable)
{
// Pause SDK
[mxSession pause];
// Update user presence
MXWeakify(self);
[self setUserPresence:MXPresenceOffline andStatusMessage:nil completion:^{
MXStrongifyAndReturnIfNil(self);
[self cancelPauseBackgroundTask];
}];
}
else
{
// Cancel pending actions
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
reachabilityObserver = nil;
MXLogDebug(@"[MXKAccount] Pause is delayed due to the session state: %@", [MXTools readableSessionState: mxSession.state]);
}
}
- (void)resume
{
if (mxSession == nil)
{
// no session to resume
return;
}
// mark that we don't want to pause anymore
self.pauseRequested = NO;
MXLogVerbose(@"[MXKAccount] resume: with session state: %@", [MXTools readableSessionState:mxSession.state]);
[self cancelBackgroundSync];
switch (mxSession.state)
{
case MXSessionStatePaused:
case MXSessionStatePauseRequested:
{
// Resume SDK and update user presence
MXWeakify(self);
[mxSession resume:^{
MXStrongifyAndReturnIfNil(self);
[self setUserPresence:self.preferredSyncPresence andStatusMessage:nil completion:nil];
[self refreshAPNSPusher];
[self refreshPushKitPusher];
}];
break;
}
case MXSessionStateStoreDataReady:
case MXSessionStateInitialSyncFailed:
{
// The session initialisation was uncompleted, we try to complete it here.
[self launchInitialServerSync];
[self refreshAPNSPusher];
[self refreshPushKitPusher];
break;
}
case MXSessionStateSyncInProgress:
{
[self refreshAPNSPusher];
[self refreshPushKitPusher];
break;
}
default:
break;
}
}
- (void)reload:(BOOL)clearCache
{
// close potential session
[self closeSession:clearCache];
if (!_disabled)
{
// Open a new matrix session
id<MXStore> store = [[[MXKAccountManager sharedManager].storeClass alloc] init];
[self openSessionWithStore:store];
}
}
#pragma mark - Push notifications
// Refresh the APNS pusher state for this account on this device.
- (void)refreshAPNSPusher
{
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher");
if (currentPusher)
{
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher aborted as a pusher has been found");
return;
}
// Check the conditions required to run the pusher
if (self.pushNotificationServiceIsActive)
{
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher: Refresh APNS pusher for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enableAPNSPusher:YES
success:nil
failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] ;: Error: %@", error);
}];
}
else if (_hasPusherForPushNotifications)
{
if ([MXKAccountManager sharedManager].apnsDeviceToken)
{
if (mxSession)
{
// Turn off pusher if user denied remote notification.
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher: Disable APNS pusher for %@ account (notifications are denied)", self.mxCredentials.userId);
[self enableAPNSPusher:NO success:nil failure:nil];
}
}
else
{
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher: APNS pusher for %@ account is already disabled. Reset _hasPusherForPushNotifications", self.mxCredentials.userId);
_hasPusherForPushNotifications = NO;
[[MXKAccountManager sharedManager] saveAccounts];
}
}
}
// Enable/Disable the APNS pusher for this account on this device on the homeserver.
- (void)enableAPNSPusher:(BOOL)enabled success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: %@", @(enabled));
#ifdef DEBUG
NSString *appId = [[NSUserDefaults standardUserDefaults] objectForKey:@"pusherAppIdDev"];
#else
NSString *appId = [[NSUserDefaults standardUserDefaults] objectForKey:@"pusherAppIdProd"];
#endif
NSString *locKey = MXKAppSettings.standardAppSettings.notificationBodyLocalizationKey;
NSDictionary *pushData = @{
@"url": self.pushGatewayURL,
@"format": @"event_id_only",
@"default_payload": @{@"aps": @{@"mutable-content": @(1), @"alert": @{@"loc-key": locKey, @"loc-args": @[]}}}
};
[self enablePusher:enabled appId:appId token:[MXKAccountManager sharedManager].apnsDeviceToken pushData:pushData success:^{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: Succeeded to update APNS pusher for %@ (%d)", self.mxCredentials.userId, enabled);
self->_hasPusherForPushNotifications = enabled;
[[MXKAccountManager sharedManager] saveAccounts];
if (enabled)
{
[self loadCurrentPusher:^{
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
} failure:^(NSError *error) {
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
}];
}
else
{
self->currentPusher = nil;
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
}
} failure:^(NSError *error) {
// Ignore error if the client try to disable an unknown token
if (!enabled)
{
// Check whether the token was unknown
MXError *mxError = [[MXError alloc] initWithNSError:error];
if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnknown])
{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: APNS was already disabled for %@!", self.mxCredentials.userId);
// Ignore the error
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
return;
}
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: Failed to disable APNS %@! (%@)", self.mxCredentials.userId, error);
}
else
{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: Failed to send APNS token for %@! (%@)", self.mxCredentials.userId, error);
}
if (failure)
{
failure(error);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
}];
}
// Refresh the PushKit pusher state for this account on this device.
- (void)refreshPushKitPusher
{
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher");
// Check the conditions required to run the pusher
if (![MXKAppSettings standardAppSettings].allowPushKitPushers)
{
// Turn off pusher if PushKit pushers are not allowed
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Disable PushKit pusher for %@ account (pushers are not allowed)", self.mxCredentials.userId);
[self enablePushKitPusher:NO success:nil failure:nil];
}
else if (self.isPushKitNotificationActive)
{
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Refresh PushKit pusher for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enablePushKitPusher:YES
success:nil
failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Error: %@", error);
}];
}
else if (self.hasPusherForPushKitNotifications)
{
if ([MXKAccountManager sharedManager].pushDeviceToken)
{
if (mxSession)
{
// Turn off pusher if user denied remote notification.
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Disable PushKit pusher for %@ account (notifications are denied)", self.mxCredentials.userId);
[self enablePushKitPusher:NO success:nil failure:nil];
}
}
else
{
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: PushKit pusher for %@ account is already disabled. Reset _hasPusherForPushKitNotifications", self.mxCredentials.userId);
self->_hasPusherForPushKitNotifications = NO;
[[MXKAccountManager sharedManager] saveAccounts];
}
}
}
// Enable/Disable the pusher based on PushKit for this account on this device on the homeserver.
- (void)enablePushKitPusher:(BOOL)enabled success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: %@", @(enabled));
if (enabled && ![MXKAppSettings standardAppSettings].allowPushKitPushers)
{
// sanity check, if accidently try to enable the pusher
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Do not enable it because PushKit pushers not allowed");
if (failure)
{
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
return;
}
NSString *appIdKey;
#ifdef DEBUG
appIdKey = @"pushKitAppIdDev";
#else
appIdKey = @"pushKitAppIdProd";
#endif
NSString *appId = [[NSUserDefaults standardUserDefaults] objectForKey:appIdKey];
NSMutableDictionary *pushData = [NSMutableDictionary dictionaryWithDictionary:@{@"url": self.pushGatewayURL}];
NSDictionary *options = [MXKAccountManager sharedManager].pushOptions;
if (options.count)
{
[pushData addEntriesFromDictionary:options];
}
NSData *token = [MXKAccountManager sharedManager].pushDeviceToken;
if (!token)
{
// sanity check, if no token there is no point of calling the endpoint
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Failed to update PushKit pusher to %@ for %@. (token is missing)", @(enabled), self.mxCredentials.userId);
if (failure)
{
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
return;
}
[self enablePusher:enabled appId:appId token:token pushData:pushData success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Succeeded to update PushKit pusher for %@. Enabled: %@. Token: %@", self.mxCredentials.userId, @(enabled), [MXKTools logForPushToken:token]);
self->_hasPusherForPushKitNotifications = enabled;
[[MXKAccountManager sharedManager] saveAccounts];
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountPushKitActivityDidChangeNotification object:self.mxCredentials.userId];
} failure:^(NSError *error) {
// Ignore error if the client try to disable an unknown token
if (!enabled)
{
// Check whether the token was unknown
MXError *mxError = [[MXError alloc] initWithNSError:error];
if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnknown])
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Push was already disabled for %@!", self.mxCredentials.userId);
// Ignore the error
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountPushKitActivityDidChangeNotification object:self.mxCredentials.userId];
return;
}
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Failed to disable Push %@! (%@)", self.mxCredentials.userId, error);
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Failed to send Push token for %@! (%@)", self.mxCredentials.userId, error);
}
if (failure)
{
failure(error);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountPushKitActivityDidChangeNotification object:self.mxCredentials.userId];
}];
}
- (void)enablePusher:(BOOL)enabled appId:(NSString*)appId token:(NSData*)token pushData:(NSDictionary*)pushData success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: %@", @(enabled));
// Refuse to try & turn push on if we're not logged in, it's nonsensical.
if (!self.mxCredentials)
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Not setting push token because we're not logged in");
return;
}
// Check whether the Push Gateway URL has been configured.
if (!self.pushGatewayURL)
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Not setting pusher because the Push Gateway URL is undefined");
return;
}
if (!appId)
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Not setting pusher because pusher app id is undefined");
return;
}
NSString *appDisplayName = [NSString stringWithFormat:@"%@ (iOS)", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]];
NSString *b64Token = [token base64EncodedStringWithOptions:0];
NSString *deviceLang = [NSLocale preferredLanguages][0];
NSString * profileTag = [[NSUserDefaults standardUserDefaults] valueForKey:@"pusherProfileTag"];
if (!profileTag)
{
profileTag = @"";
NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < 16; ++i)
{
unsigned char c = [alphabet characterAtIndex:arc4random() % alphabet.length];
profileTag = [profileTag stringByAppendingFormat:@"%c", c];
}
MXLogDebug(@"[MXKAccount][Push] enablePusher: Generated fresh profile tag: %@", profileTag);
[[NSUserDefaults standardUserDefaults] setValue:profileTag forKey:@"pusherProfileTag"];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Using existing profile tag: %@", profileTag);
}
NSObject *kind = enabled ? @"http" : [NSNull null];
// Use the append flag to handle multiple accounts registration.
BOOL append = NO;
// Check whether a pusher is running for another account
NSArray *activeAccounts = [MXKAccountManager sharedManager].activeAccounts;
for (MXKAccount *account in activeAccounts)
{
if (![account.mxCredentials.userId isEqualToString:self.mxCredentials.userId] && account.pushNotificationServiceIsActive)
{
append = YES;
break;
}
}
MXLogDebug(@"[MXKAccount][Push] enablePusher: append flag: %d", append);
MXRestClient *restCli = self.mxRestClient;
[restCli setPusherWithPushkey:b64Token kind:kind appId:appId appDisplayName:appDisplayName deviceDisplayName:[[UIDevice currentDevice] name] profileTag:profileTag lang:deviceLang data:pushData append:append enabled:enabled success:success failure:failure];
}
#pragma mark - InApp notifications
- (void)listenToNotifications:(MXOnNotification)onNotification
{
// Check conditions required to add notification listener
if (!mxSession || !onNotification)
{
return;
}
// Remove existing listener (if any)
[self removeNotificationListener];
// Register on notification center
notificationCenterListener = [self.mxSession.notificationCenter listenToNotifications:^(MXEvent *event, MXRoomState *roomState, MXPushRule *rule)
{
// Apply first the event filter defined in the related room data source
MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self->mxSession];
[roomDataSourceManager roomDataSourceForRoom:event.roomId create:NO onComplete:^(MXKRoomDataSource *roomDataSource) {
if (roomDataSource)
{
if (!roomDataSource.eventFormatter.eventTypesFilterForMessages || [roomDataSource.eventFormatter.eventTypesFilterForMessages indexOfObject:event.type] != NSNotFound)
{
// Check conditions to report this notification
if (nil == self->ignoredRooms || [self->ignoredRooms indexOfObject:event.roomId] == NSNotFound)
{
onNotification(event, roomState, rule);
}
}
}
}];
}];
}
- (void)removeNotificationListener
{
if (notificationCenterListener)
{
[self.mxSession.notificationCenter removeListener:notificationCenterListener];
notificationCenterListener = nil;
}
ignoredRooms = nil;
}
- (void)updateNotificationListenerForRoomId:(NSString*)roomID ignore:(BOOL)isIgnored
{
if (isIgnored)
{
if (!ignoredRooms)
{
ignoredRooms = [[NSMutableArray alloc] init];
}
[ignoredRooms addObject:roomID];
}
else if (ignoredRooms)
{
[ignoredRooms removeObject:roomID];
}
}
#pragma mark - Internals
- (void)launchInitialServerSync
{
// Complete the session registration when store data is ready.
// Cancel potential reachability observer and pending action
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
reachabilityObserver = nil;
// Sanity check
if (!mxSession || (mxSession.state != MXSessionStateStoreDataReady && mxSession.state != MXSessionStateInitialSyncFailed))
{
MXLogDebug(@"[MXKAccount] Initial server sync is applicable only when store data is ready to complete session initialisation");
return;
}
// Use /sync filter corresponding to current settings and homeserver capabilities
MXWeakify(self);
[self buildSyncFilter:^(MXFilterJSONModel *syncFilter) {
MXStrongifyAndReturnIfNil(self);
// Make sure the filter is compatible with the previously used one
MXWeakify(self);
[self checkSyncFilterCompatibility:syncFilter completion:^(BOOL compatible) {
MXStrongifyAndReturnIfNil(self);
if (!compatible)
{
// Else clear the cache
MXLogDebug(@"[MXKAccount] New /sync filter not compatible with previous one. Clear cache");
[self reload:YES];
return;
}
// Launch mxSession
MXWeakify(self);
[self.mxSession startWithSyncFilter:syncFilter onServerSyncDone:^{
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[MXKAccount] %@: The session is ready. Matrix SDK session has been started in %0.fms.", self.mxCredentials.userId, [[NSDate date] timeIntervalSinceDate:self->openSessionStartDate] * 1000);
[self setUserPresence:self.preferredSyncPresence andStatusMessage:nil completion:nil];
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[MXKAccount] Initial Sync failed. Error: %@", error);
BOOL isClientTimeout = [error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorTimedOut;
NSHTTPURLResponse *httpResponse = [MXHTTPOperation urlResponseFromError:error];
BOOL isServerTimeout = httpResponse && [initialSyncSilentErrorsHTTPStatusCodes containsObject:@(httpResponse.statusCode)];
if (isClientTimeout || isServerTimeout)
{
// do not propagate this error to the client
// the request will be retried or postponed according to the reachability status
MXLogDebug(@"[MXKAccount] Initial sync failure did not propagated");
}
else if (self->notifyOpenSessionFailure && error)
{
// Notify MatrixKit user only once
self->notifyOpenSessionFailure = NO;
NSString *myUserId = self.mxSession.myUser.userId;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
}
// Check if it is a network connectivity issue
AFNetworkReachabilityManager *networkReachabilityManager = [AFNetworkReachabilityManager sharedManager];
MXLogDebug(@"[MXKAccount] Network reachability: %d", networkReachabilityManager.isReachable);
if (networkReachabilityManager.isReachable)
{
// If we have network, we retry immediately, otherwise the server may clear any cache it has computed thus far
[self launchInitialServerSync];
}
else
{
// The device is not connected to the internet, wait for the connection to be up again before retrying
// Add observer to launch a new attempt according to reachability.
self->reachabilityObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingReachabilityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
NSNumber *statusItem = note.userInfo[AFNetworkingReachabilityNotificationStatusItem];
if (statusItem)
{
AFNetworkReachabilityStatus reachabilityStatus = statusItem.integerValue;
if (reachabilityStatus == AFNetworkReachabilityStatusReachableViaWiFi || reachabilityStatus == AFNetworkReachabilityStatusReachableViaWWAN)
{
// New attempt
[self launchInitialServerSync];
}
}
}];
}
}];
}];
}];
}
- (void)attemptDeviceDehydrationWithKeyData:(NSData *)keyData
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
[self attemptDeviceDehydrationWithKeyData:keyData retry:YES success:success failure:failure];
}
- (void)attemptDeviceDehydrationWithKeyData:(NSData *)keyData
retry:(BOOL)retry
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
if (keyData == nil)
{
MXLogWarning(@"[MXKAccount] attemptDeviceDehydrationWithRetry: no key provided for device dehydration");
if (failure)
{
failure(nil);
}
return;
}
if (![mxSession.crypto.crossSigning isKindOfClass:[MXLegacyCrossSigning class]]) {
MXLogFailure(@"Device dehydratation is currently only supported by legacy cross signing, add support to all implementations");
if (failure)
{
failure(nil);
}
return;
}
MXLegacyCrossSigning *crossSigning = (MXLegacyCrossSigning *)mxSession.crypto.crossSigning;;
MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: starting device dehydration");
[[MXKAccountManager sharedManager].dehydrationService dehydrateDeviceWithMatrixRestClient:mxRestClient crossSigning:crossSigning dehydrationKey:keyData success:^(NSString *deviceId) {
MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device successfully dehydrated");
if (success)
{
success();
}
} failure:^(NSError *error) {
if (retry)
{
[self attemptDeviceDehydrationWithKeyData:keyData retry:NO success:success failure:failure];
MXLogErrorDetails(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error: Retrying.", @{
@"error": error ?: @"unknown"
});
}
else
{
MXLogErrorDetails(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error", @{
@"error": error ?: @"unknown"
});
if (failure)
{
failure(error);
}
}
}];
}
- (void)onMatrixSessionStateChange
{
// Check if pause has been requested
if (self.isPauseRequested && mxSession.isPauseable)
{
MXLogDebug(@"[MXKAccount] Apply the pending pause.");
[self pauseInBackgroundTask];
return;
}
if (mxSession.state == MXSessionStateRunning)
{
// Check whether the session was not already running
if (!userUpdateListener)
{
// Register listener to user's information change
userUpdateListener = [mxSession.myUser listenToUserUpdate:^(MXEvent *event) {
// Consider events related to user's presence
if (event.eventType == MXEventTypePresence)
{
self->userPresence = [MXTools presence:event.content[@"presence"]];
}
// Here displayname or other information have been updated, post update notification.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}];
// User information are just up-to-date (`mxSession` is running), post update notification.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
}
else if (mxSession.state == MXSessionStateStoreDataReady || mxSession.state == MXSessionStateSyncInProgress)
{
// Remove listener (if any), this action is required to handle correctly matrix sdk handler reload (see clear cache)
if (userUpdateListener)
{
[mxSession.myUser removeListener:userUpdateListener];
userUpdateListener = nil;
}
else
{
// Here the initial server sync is in progress. The session is not running yet, but some user's information are available (from local storage).
// We post update notification to let observer take into account this user's information even if they may not be up-to-date.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
}
else if (mxSession.state == MXSessionStatePaused)
{
self.pauseRequested = NO;
}
}
- (void)prepareRESTClient
{
if (!self.mxCredentials)
{
return;
}
MXWeakify(self);
mxRestClient = [[MXRestClient alloc] initWithCredentials:self.mxCredentials andOnUnrecognizedCertificateBlock:^BOOL(NSData *certificate) {
MXStrongifyAndReturnValueIfNil(self, NO);
if (_onCertificateChangeBlock)
{
if (_onCertificateChangeBlock (self, certificate))
{
// Update the certificate in credentials
self.mxCredentials.allowedCertificate = certificate;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
return YES;
}
self.mxCredentials.ignoredCertificate = certificate;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
return NO;
} andPersistentTokenDataHandler:^(void (^handler)(NSArray<MXCredentials *> *credentials, void (^completion)(BOOL didUpdateCredentials))) {
[MXKAccountManager.sharedManager readAndWriteCredentials:handler];
} andUnauthenticatedHandler:^(MXError *error, BOOL isSoftLogout, BOOL isRefreshTokenAuth, void (^completion)(void)) {
MXStrongifyAndReturnIfNil(self);
[self handleUnauthenticatedWithError:error isSoftLogout:isSoftLogout isRefreshTokenAuth:isRefreshTokenAuth andCompletion:completion];
}];
}
- (void)handleUnauthenticatedWithError:(MXError *)error isSoftLogout:(BOOL)isSoftLogout isRefreshTokenAuth:(BOOL)isRefreshTokenAuth andCompletion:(void (^)(void))completion
{
[Analytics.shared trackAuthUnauthenticatedErrorWithSoftLogout:isSoftLogout refreshTokenAuth:isRefreshTokenAuth errorCode:error.errcode errorReason:error.error];
MXLogDebug(@"[MXKAccountManager] handleUnauthenticated: trackAuthUnauthenticatedErrorWithSoftLogout sent");
if (isSoftLogout)
{
MXLogDebug(@"[MXKAccountManager] handleUnauthenticated: soft logout.");
[[MXKAccountManager sharedManager] softLogout:self];
completion();
}
else
{
MXLogDebug(@"[MXKAccountManager] handleUnauthenticated: hard logout.");
[[MXKAccountManager sharedManager] removeAccount:self sendLogoutRequest:NO completion:completion];
}
}
- (void)onDateTimeFormatUpdate
{
if ([mxSession.roomSummaryUpdateDelegate isKindOfClass:MXKEventFormatter.class])
{
MXKEventFormatter *eventFormatter = (MXKEventFormatter*)mxSession.roomSummaryUpdateDelegate;
// Update the date and time formatters
[eventFormatter initDateTimeFormatters];
dispatch_group_t dispatchGroup = dispatch_group_create();
for (MXRoom *room in mxSession.rooms)
{
MXRoomSummary *summary = room.summary;
if (summary)
{
dispatch_group_enter(dispatchGroup);
[summary.mxSession eventWithEventId:summary.lastMessage.eventId
inRoom:summary.roomId
success:^(MXEvent *event) {
if (event)
{
if (summary.lastMessage.others == nil)
{
summary.lastMessage.others = [NSMutableDictionary dictionary];
}
summary.lastMessage.others[@"lastEventDate"] = [eventFormatter dateStringFromEvent:event withTime:YES];
[self->mxSession.store.roomSummaryStore storeSummary:summary];
}
dispatch_group_leave(dispatchGroup);
} failure:^(NSError *error) {
MXLogErrorDetails(@"[MXKAccount] onDateTimeFormatUpdate: event fetch failed", @{
@"error": error ?: @"unknown"
});
dispatch_group_leave(dispatchGroup);
}];
}
}
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{
// Commit store changes done
if ([self->mxSession.store respondsToSelector:@selector(commit)])
{
[self->mxSession.store commit];
}
// Broadcast the change which concerns all the room summaries.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXRoomSummaryDidChangeNotification object:nil userInfo:nil];
});
}
}
- (void)cancelPauseBackgroundTask
{
// Cancel background task
if (self.pauseBackgroundTask.isRunning)
{
[self.pauseBackgroundTask stop];
self.pauseBackgroundTask = nil;
}
}
#pragma mark - Crypto
- (void)resetDeviceId
{
self.mxCredentials.deviceId = nil;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
#pragma mark - backgroundSync management
- (void)cancelBackgroundSync
{
if (self.backgroundSyncBgTask.isRunning)
{
MXLogDebug(@"[MXKAccount] The background Sync is cancelled.");
if (mxSession)
{
if (mxSession.state == MXSessionStateBackgroundSyncInProgress)
{
[mxSession pause];
}
}
[self onBackgroundSyncDone:[NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]];
}
}
- (void)onBackgroundSyncDone:(NSError*)error
{
if (backgroundSyncTimer)
{
[backgroundSyncTimer invalidate];
backgroundSyncTimer = nil;
}
if (backgroundSyncFails && error)
{
backgroundSyncFails(error);
}
if (backgroundSyncDone && !error)
{
backgroundSyncDone();
}
backgroundSyncDone = nil;
backgroundSyncFails = nil;
// End background task
if (self.backgroundSyncBgTask.isRunning)
{
[self.backgroundSyncBgTask stop];
self.backgroundSyncBgTask = nil;
}
}
- (void)onBackgroundSyncTimerOut
{
[self cancelBackgroundSync];
}
- (void)backgroundSync:(unsigned int)timeout success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
// Check whether a background mode handler has been set.
id<MXBackgroundModeHandler> handler = [MXSDKOptions sharedInstance].backgroundModeHandler;
if (handler)
{
// Only work when the application is suspended.
// Check conditions before launching background sync
if (mxSession && mxSession.state == MXSessionStatePaused)
{
MXLogDebug(@"[MXKAccount] starts a background Sync");
backgroundSyncDone = success;
backgroundSyncFails = failure;
MXWeakify(self);
self.backgroundSyncBgTask = [handler startBackgroundTaskWithName:@"[MXKAccount] backgroundSync:success:failure:" expirationHandler:^{
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[MXKAccount] the background Sync fails because of the bg task timeout");
[self cancelBackgroundSync];
}];
// ensure that the backgroundSync will be really done in the expected time
// the request could be done but the treatment could be long so add a timer to cancel it
// if it takes too much time
backgroundSyncTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:(timeout - 1) / 1000]
interval:0
target:self
selector:@selector(onBackgroundSyncTimerOut)
userInfo:nil
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:backgroundSyncTimer forMode:NSDefaultRunLoopMode];
[mxSession backgroundSync:timeout success:^{
MXLogDebug(@"[MXKAccount] the background Sync succeeds");
[self onBackgroundSyncDone:nil];
}
failure:^(NSError* error) {
MXLogDebug(@"[MXKAccount] the background Sync fails");
[self onBackgroundSyncDone:error];
}
];
}
else
{
MXLogDebug(@"[MXKAccount] cannot start background Sync (invalid state %@)", [MXTools readableSessionState:mxSession.state]);
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
}
else
{
MXLogDebug(@"[MXKAccount] cannot start background Sync");
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
}
#pragma mark - Sync filter
- (void)supportLazyLoadOfRoomMembersWithMatrixVersion:(MXMatrixVersions *)matrixVersions
completion:(void (^)(BOOL supportLazyLoadOfRoomMembers))completion
{
void(^onUnsupportedLazyLoadOfRoomMembers)(NSError *) = ^(NSError *error) {
completion(NO);
};
// Check if the server supports LL sync filter
MXFilterJSONModel *filter = [self syncFilterWithLazyLoadOfRoomMembers:YES supportsNotificationsForThreads:NO];
[mxSession.store filterIdForFilter:filter success:^(NSString * _Nullable filterId) {
if (filterId)
{
// The LL filter is already in the store. The HS supports LL
completion(YES);
}
else
{
// Check the Matrix versions supported by the HS
if (matrixVersions)
{
if (matrixVersions.supportLazyLoadMembers)
{
// The HS supports LL
completion(YES);
}
else
{
onUnsupportedLazyLoadOfRoomMembers(nil);
}
}
else
{
completion(NO);
}
}
} failure:onUnsupportedLazyLoadOfRoomMembers];
}
/**
Build the sync filter according to application settings and HS capability.
@param completion the block providing the sync filter to use.
*/
- (void)buildSyncFilter:(void (^)(MXFilterJSONModel *syncFilter))completion
{
// Check settings
BOOL syncWithLazyLoadOfRoomMembersSetting = [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers;
void(^buildSyncFilter)(MXMatrixVersions *) = ^(MXMatrixVersions *matrixVersions) {
BOOL supportsNotificationsForThreads = matrixVersions ? matrixVersions.supportsNotificationsForThreads : NO;
if (syncWithLazyLoadOfRoomMembersSetting)
{
// Check if the server supports LL sync filter before enabling it
[self supportLazyLoadOfRoomMembersWithMatrixVersion:matrixVersions completion:^(BOOL supportLazyLoadOfRoomMembers) {
if (supportLazyLoadOfRoomMembers)
{
completion([self syncFilterWithLazyLoadOfRoomMembers:YES
supportsNotificationsForThreads:supportsNotificationsForThreads]);
}
else
{
// No support from the HS
// Disable the setting. That will avoid to make a request at every startup
[MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers = NO;
completion([self syncFilterWithLazyLoadOfRoomMembers:NO
supportsNotificationsForThreads:supportsNotificationsForThreads]);
}
}];
}
else
{
completion([self syncFilterWithLazyLoadOfRoomMembers:NO supportsNotificationsForThreads:supportsNotificationsForThreads]);
}
};
[mxSession supportedMatrixVersions:^(MXMatrixVersions *matrixVersions) {
buildSyncFilter(matrixVersions);
} failure:^(NSError *error) {
MXLogWarning(@"[MXAccount] buildSyncFilter: failed to get supported versions: %@", error);
buildSyncFilter(nil);
}];
}
/**
Compute the sync filter to use according to the device screen size.
@param syncWithLazyLoadOfRoomMembers enable LL support.
@return the sync filter to use.
*/
- (MXFilterJSONModel *)syncFilterWithLazyLoadOfRoomMembers:(BOOL)syncWithLazyLoadOfRoomMembers supportsNotificationsForThreads:(BOOL)supportsNotificationsForThreads
{
MXFilterJSONModel *syncFilter;
NSUInteger limit = 10;
// Define a message limit for /sync requests that is high enough so that
// a full page of room messages can be displayed without an additional
// server request.
// This limit value depends on the device screen size. So, the rough rule is:
// - use 10 for small phones (5S/SE)
// - use 15 for phones (6/6S/7/8)
// - use 20 for phablets (.Plus/X/XR/XS/XSMax)
// - use 30 for iPads
UIUserInterfaceIdiom userInterfaceIdiom = [[UIDevice currentDevice] userInterfaceIdiom];
if (userInterfaceIdiom == UIUserInterfaceIdiomPhone)
{
CGFloat screenHeight = [[UIScreen mainScreen] nativeBounds].size.height;
if (screenHeight == 1334) // 6/6S/7/8 screen height
{
limit = 15;
}
else if (screenHeight > 1334)
{
limit = 20;
}
}
else if (userInterfaceIdiom == UIUserInterfaceIdiomPad)
{
limit = 30;
}
// Set that limit in the filter
if (syncWithLazyLoadOfRoomMembers)
{
syncFilter = [MXFilterJSONModel syncFilterForLazyLoadingWithMessageLimit:limit unreadThreadNotifications:supportsNotificationsForThreads];
}
else
{
syncFilter = [MXFilterJSONModel syncFilterWithMessageLimit:limit unreadThreadNotifications:supportsNotificationsForThreads];
}
// TODO: We could extend the filter to match other settings (self.showAllEventsInRoomHistory,
// self.eventsFilterForMessages, etc).
return syncFilter;
}
/**
Check the sync filter we want to use is compatible with the one previously used.
@param syncFilter the sync filter to use.
@param completion the block called to indicated the compatibility.
*/
- (void)checkSyncFilterCompatibility:(MXFilterJSONModel*)syncFilter completion:(void (^)(BOOL compatible))completion
{
// There is no compatibility issue if no /sync was done before
if (!mxSession.store.eventStreamToken)
{
completion(YES);
}
// Check the filter we want to use is compatible with the one previously used
else if (!syncFilter && !mxSession.syncFilterId)
{
// A nil filter implies a nil mxSession.syncFilterId. So, there is no filter change
completion(YES);
}
else if (!syncFilter || !mxSession.syncFilterId)
{
// Change from no filter with using a filter or vice-versa. So, there is a filter change
MXLogDebug(@"[MXKAccount] checkSyncFilterCompatibility: Incompatible filter. New or old is nil. mxSession.syncFilterId: %@ - syncFilter: %@",
mxSession.syncFilterId, syncFilter.JSONDictionary);
completion(NO);
}
else if (!mxSession.store.allFilterIds.count)
{
MXLogDebug(@"[MXKAccount] There are no filters stored in this session, proceed as if no /sync was done before");
completion(YES);
}
else
{
// Check the filter is the one previously set
// It must be already in the store
MXWeakify(self);
[mxSession.store filterIdForFilter:syncFilter success:^(NSString * _Nullable filterId) {
MXStrongifyAndReturnIfNil(self);
// Note: We could be more tolerant here
// We could accept filter hot change if the change is limited to the `limit` filter value
// But we do not have this requirement yet
BOOL compatible = [filterId isEqualToString:self.mxSession.syncFilterId];
if (!compatible)
{
MXLogDebug(@"[MXKAccount] checkSyncFilterCompatibility: Incompatible filter ids. mxSession.syncFilterId: %@ - store.filterId: %@ - syncFilter: %@",
self.mxSession.syncFilterId, filterId, syncFilter.JSONDictionary);
}
completion(compatible);
} failure:^(NSError * _Nullable error) {
// Should never happen
completion(NO);
}];
}
}
#pragma mark - Identity server updates
- (void)registerAccountDataDidChangeIdentityServerNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccountDataDidChangeIdentityServerNotification:) name:kMXSessionAccountDataDidChangeIdentityServerNotification object:nil];
}
- (void)handleAccountDataDidChangeIdentityServerNotification:(NSNotification*)notification
{
MXSession *mxSession = notification.object;
if (mxSession == self.mxSession)
{
if (![self.mxCredentials.identityServer isEqualToString:self.mxSession.accountDataIdentityServer])
{
_identityServerURL = self.mxSession.accountDataIdentityServer;
self.mxCredentials.identityServer = _identityServerURL;
self.mxCredentials.identityServerAccessToken = nil;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
}
}
#pragma mark - Identity server Access Token updates
- (void)identityService:(MXIdentityService *)identityService didUpdateAccessToken:(NSString *)accessToken
{
self.mxCredentials.identityServerAccessToken = accessToken;
}
- (void)registerIdentityServiceDidChangeAccessTokenNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityServiceDidChangeAccessTokenNotification:) name:MXIdentityServiceDidChangeAccessTokenNotification object:nil];
}
- (void)handleIdentityServiceDidChangeAccessTokenNotification:(NSNotification*)notification
{
NSDictionary *userInfo = notification.userInfo;
NSString *userId = userInfo[MXIdentityServiceNotificationUserIdKey];
NSString *identityServer = userInfo[MXIdentityServiceNotificationIdentityServerKey];
NSString *accessToken = userInfo[MXIdentityServiceNotificationAccessTokenKey];
if (userId && identityServer && accessToken && [self.mxCredentials.identityServer isEqualToString:identityServer])
{
self.mxCredentials.identityServerAccessToken = accessToken;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
}
#pragma mark - Presence
- (void)setPreferredSyncPresence:(MXPresence)preferredSyncPresence
{
[super setPreferredSyncPresence:preferredSyncPresence];
if (self.mxSession)
{
self.mxSession.preferredSyncPresence = preferredSyncPresence;
[self setUserPresence:preferredSyncPresence andStatusMessage:nil completion:nil];
}
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
@end