diff --git a/CHANGES.md b/CHANGES.md
index 040d7e5b4..c7df60542 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,58 @@
+## Changes in 1.8.8 (2022-03-22)
+
+✨ Features
+
+- Invite to Space in room landing ([#5225](https://github.com/vector-im/element-ios/issues/5225))
+- Implement FAB journeys & rough edge warnings ([#5226](https://github.com/vector-im/element-ios/issues/5226))
+- Space panel overflow journeys & rough edge warnings ([#5227](https://github.com/vector-im/element-ios/issues/5227))
+- Let people know when rooms have moved. ([#5228](https://github.com/vector-im/element-ios/issues/5228))
+- Room Settings bottom sheet ([#5229](https://github.com/vector-im/element-ios/issues/5229))
+- Adding Rooms to Spaces ([#5230](https://github.com/vector-im/element-ios/issues/5230))
+- Spaces: Update room settings for Spaces ([#5231](https://github.com/vector-im/element-ios/issues/5231))
+- Spaces: Long press on rooms in space room lists ([#5232](https://github.com/vector-im/element-ios/issues/5232))
+- Space Settings ([#5233](https://github.com/vector-im/element-ios/issues/5233))
+
+🙌 Improvements
+
+- Upgrade MatrixSDK version ([v0.23.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.0)).
+- Space creation: Added entire space creation flow. ([#5224](https://github.com/vector-im/element-ios/issues/5224))
+- Instrument metrics for the IA project. ([#5401](https://github.com/vector-im/element-ios/issues/5401))
+- RoomDataSource: Reload thread screen for the first message. ([#5441](https://github.com/vector-im/element-ios/issues/5441))
+- Change behaviour of avatar/self in left menu to match common paradigm and take user to their own profile/settings ([#5500](https://github.com/vector-im/element-ios/issues/5500))
+- Secure Backup: Add support for mandatory backup/verification ([#5745](https://github.com/vector-im/element-ios/issues/5745))
+- Thread Notifications: Open thread & reply to thread from notifications. ([#5749](https://github.com/vector-im/element-ios/issues/5749))
+- IA Metrics: added trigger to JoinedRoom event and implemented ViewRoom event ([#5769](https://github.com/vector-im/element-ios/issues/5769))
+- Activity Indicators: Replace user indicator presenting view controller with context ([#5780](https://github.com/vector-im/element-ios/issues/5780))
+- MXKEventFormatter: Extend reply fallback for also non-thread events. ([#5816](https://github.com/vector-im/element-ios/issues/5816))
+- Location sharing: Support multiple user annotation views on the map. ([#5827](https://github.com/vector-im/element-ios/issues/5827))
+- MXKRoomDataSource: Pass threadId of room data source for replies. ([#5829](https://github.com/vector-im/element-ios/issues/5829))
+- MXKEventFormatter: Fix edit fallback usage for edited events. ([#5841](https://github.com/vector-im/element-ios/issues/5841))
+- RoomViewController: Remove thread list bar button item badge count. ([#5853](https://github.com/vector-im/element-ios/issues/5853))
+
+🐛 Bugfixes
+
+- Fix user suggestions not showing up when re-entering a room. ([#5876](https://github.com/vector-im/element-ios/pull/5876))
+- Prevent the homescreen from resetting on every appearance. ([#5885](https://github.com/vector-im/element-ios/pull/5885))
+- UserSuggestionViewModel: Fix retain cycle ([#5058](https://github.com/vector-im/element-ios/issues/5058))
+- Green launch spinner is sometimes dismissed too early causing the incorrect onboarding screen to be displayed. ([#5472](https://github.com/vector-im/element-ios/issues/5472))
+- Home: Fix crash when pressing tabs ([#5547](https://github.com/vector-im/element-ios/issues/5547))
+- Selection impossible when filtering in add room screen. ([#5757](https://github.com/vector-im/element-ios/issues/5757))
+- Room: Refresh header when call actions become available (member count changes) ([#5800](https://github.com/vector-im/element-ios/issues/5800))
+- Share Extension: Stop logging crashes due to intentional exception that frees up memory and handle changes to MXRoom in the SDK. ([#5805](https://github.com/vector-im/element-ios/issues/5805))
+- Crash after leaving last space. ([#5825](https://github.com/vector-im/element-ios/issues/5825))
+- Authentication: Fix a crash that occurred when using the app with an account that had a soft logout. ([#5846](https://github.com/vector-im/element-ios/issues/5846))
+- MXAccount: Do not clear cache if there are no stored filters ([#5873](https://github.com/vector-im/element-ios/issues/5873))
+
+⚠️ API Changes
+
+- Rename scrollEdgesAppearance → scrollEdgeAppearance to match UIKit. ([#5826](https://github.com/vector-im/element-ios/pull/5826))
+
+🚧 In development 🚧
+
+- Onboarding: Add screens for setting a display name and avatar when signing up for the first time. ([#5652](https://github.com/vector-im/element-ios/issues/5652))
+- Location sharing: Handle live location banner view in room screen. ([#5857](https://github.com/vector-im/element-ios/issues/5857))
+
+
## Changes in 1.8.7 (2022-03-18)
🙌 Improvements
diff --git a/CommonKit/Source/UserIndicators/UserIndicatorPresentationContext.swift b/CommonKit/Source/UserIndicators/UserIndicatorPresentationContext.swift
new file mode 100644
index 000000000..94656ad6b
--- /dev/null
+++ b/CommonKit/Source/UserIndicators/UserIndicatorPresentationContext.swift
@@ -0,0 +1,42 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+import UIKit
+
+/// The presentation context is used by `UserIndicatorViewPresentable`s to display content
+/// on the screen and it serves two primary purposes:
+///
+/// - abstraction on top of UIKit (passing context instead of view controllers)
+/// - immutable context passed at init with variable presenting view controller
+/// (e.g. depending on collapsed / uncollapsed iPad presentation that changes
+/// at runtime)
+public protocol UserIndicatorPresentationContext {
+ var indicatorPresentingViewController: UIViewController? { get }
+}
+
+/// A simple implementation of `UserIndicatorPresentationContext` that uses a weak reference
+/// to the passed-in view controller as the presentation context.
+public class StaticUserIndicatorPresentationContext: UserIndicatorPresentationContext {
+ // The presenting view controller will be the parent of the user indicator,
+ // and the indicator holds a strong reference to the context, so the view controller
+ // must be decleared `weak` to avoid a retain cycle
+ public private(set) weak var indicatorPresentingViewController: UIViewController?
+
+ public init(viewController: UIViewController) {
+ self.indicatorPresentingViewController = viewController
+ }
+}
diff --git a/CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift b/CommonKit/Source/UserIndicators/UserIndicatorViewPresentable.swift
similarity index 100%
rename from CommonKit/Source/UserIndicators/UserIndicatorPresentable.swift
rename to CommonKit/Source/UserIndicators/UserIndicatorViewPresentable.swift
diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig
index 164b076c5..e2f771c27 100644
--- a/Config/AppVersion.xcconfig
+++ b/Config/AppVersion.xcconfig
@@ -15,5 +15,5 @@
//
// Version
-MARKETING_VERSION = 1.8.7
-CURRENT_PROJECT_VERSION = 1.8.7
+MARKETING_VERSION = 1.8.8
+CURRENT_PROJECT_VERSION = 1.8.8
diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift
index b9c639f15..8d3767215 100644
--- a/Config/BuildSettings.swift
+++ b/Config/BuildSettings.swift
@@ -369,7 +369,7 @@ final class BuildSettings: NSObject {
static let authEnableRefreshTokens = false
// MARK: - Onboarding
- static let onboardingShowAccountPersonalisation = false
+ static let onboardingShowAccountPersonalization = false
// MARK: - Unified Search
static let unifiedSearchScreenShowPublicDirectory = true
diff --git a/DesignKit/Variants/Fonts/ElementFonts.swift b/DesignKit/Variants/Fonts/ElementFonts.swift
index d7538c905..e17984389 100644
--- a/DesignKit/Variants/Fonts/ElementFonts.swift
+++ b/DesignKit/Variants/Fonts/ElementFonts.swift
@@ -110,10 +110,10 @@ extension ElementFonts: Fonts {
public var title2: SharedFont {
let uiFont = self.font(forTextStyle: .title2)
- if #available(iOS 13.0, *) {
- return SharedFont(uiFont: uiFont, font: Font(uiFont))
- } else if #available(iOS 14.0, *) {
+ if #available(iOS 14.0, *) {
return SharedFont(uiFont: uiFont, font: .title2)
+ } else if #available(iOS 13.0, *) {
+ return SharedFont(uiFont: uiFont, font: Font(uiFont))
} else {
return SharedFont(uiFont: uiFont)
}
@@ -122,10 +122,10 @@ extension ElementFonts: Fonts {
public var title2B: SharedFont {
let uiFont = self.title2.uiFont.vc_bold
- if #available(iOS 13.0, *) {
- return SharedFont(uiFont: uiFont, font: Font(uiFont))
- } else if #available(iOS 14.0, *) {
+ if #available(iOS 14.0, *) {
return SharedFont(uiFont: uiFont, font: .title2.bold())
+ } else if #available(iOS 13.0, *) {
+ return SharedFont(uiFont: uiFont, font: Font(uiFont))
} else {
return SharedFont(uiFont: uiFont)
}
@@ -134,10 +134,10 @@ extension ElementFonts: Fonts {
public var title3: SharedFont {
let uiFont = self.font(forTextStyle: .title3)
- if #available(iOS 13.0, *) {
- return SharedFont(uiFont: uiFont, font: Font(uiFont))
- } else if #available(iOS 14.0, *) {
+ if #available(iOS 14.0, *) {
return SharedFont(uiFont: uiFont, font: .title3)
+ } else if #available(iOS 13.0, *) {
+ return SharedFont(uiFont: uiFont, font: Font(uiFont))
} else {
return SharedFont(uiFont: uiFont)
}
@@ -146,10 +146,10 @@ extension ElementFonts: Fonts {
public var title3SB: SharedFont {
let uiFont = self.title3.uiFont.vc_semiBold
- if #available(iOS 13.0, *) {
- return SharedFont(uiFont: uiFont, font: Font(uiFont))
- } else if #available(iOS 14.0, *) {
+ if #available(iOS 14.0, *) {
return SharedFont(uiFont: uiFont, font: .title3.weight(.semibold))
+ } else if #available(iOS 13.0, *) {
+ return SharedFont(uiFont: uiFont, font: Font(uiFont))
} else {
return SharedFont(uiFont: uiFont)
}
@@ -258,10 +258,10 @@ extension ElementFonts: Fonts {
public var caption2: SharedFont {
let uiFont = self.font(forTextStyle: .caption2)
- if #available(iOS 13.0, *) {
- return SharedFont(uiFont: uiFont, font: Font(uiFont))
- } else if #available(iOS 14.0, *) {
+ if #available(iOS 14.0, *) {
return SharedFont(uiFont: uiFont, font: .caption2)
+ } else if #available(iOS 13.0, *) {
+ return SharedFont(uiFont: uiFont, font: Font(uiFont))
} else {
return SharedFont(uiFont: uiFont)
}
@@ -270,10 +270,10 @@ extension ElementFonts: Fonts {
public var caption2SB: SharedFont {
let uiFont = self.caption2.uiFont.vc_semiBold
- if #available(iOS 13.0, *) {
- return SharedFont(uiFont: uiFont, font: Font(uiFont))
- } else if #available(iOS 14.0, *) {
+ if #available(iOS 14.0, *) {
return SharedFont(uiFont: uiFont, font: .caption2.weight(.semibold))
+ } else if #available(iOS 13.0, *) {
+ return SharedFont(uiFont: uiFont, font: Font(uiFont))
} else {
return SharedFont(uiFont: uiFont)
}
diff --git a/Gemfile.lock b/Gemfile.lock
index 610cbbadd..edfd3f855 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,7 +3,7 @@ GEM
specs:
CFPropertyList (3.0.5)
rexml
- activesupport (6.1.4.4)
+ activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -17,27 +17,27 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
- aws-partitions (1.541.0)
- aws-sdk-core (3.124.0)
+ aws-partitions (1.568.0)
+ aws-sdk-core (3.130.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.52.0)
- aws-sdk-core (~> 3, >= 3.122.0)
+ aws-sdk-kms (1.55.0)
+ aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.109.0)
- aws-sdk-core (~> 3, >= 3.122.0)
+ aws-sdk-s3 (1.113.0)
+ aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.0.3)
- cocoapods (1.11.2)
+ cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
- cocoapods-core (= 1.11.2)
+ cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -52,7 +52,7 @@ GEM
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
- cocoapods-core (1.11.2)
+ cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
@@ -86,17 +86,18 @@ GEM
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
- excon (0.89.0)
- faraday (1.8.0)
+ excon (0.92.1)
+ faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
- faraday-httpclient (~> 1.0.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
- faraday-net_http_persistent (~> 1.1)
+ faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
- multipart-post (>= 1.2, < 3)
+ faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
@@ -105,14 +106,17 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.3)
+ multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
- fastlane (2.199.0)
+ fastlane (2.205.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -157,13 +161,13 @@ GEM
fastlane-plugin-versioning (0.5.0)
fastlane-plugin-xcodegen (1.1.0)
fastlane-plugin-brew (~> 0.1.1)
- ffi (1.15.4)
+ ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
- google-apis-androidpublisher_v3 (0.14.0)
+ google-apis-androidpublisher_v3 (0.16.0)
google-apis-core (>= 0.4, < 2.a)
- google-apis-core (0.4.1)
+ google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -172,11 +176,11 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
- google-apis-iamcredentials_v1 (0.9.0)
+ google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
- google-apis-playcustomapp_v1 (0.6.0)
+ google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
- google-apis-storage_v1 (0.10.0)
+ google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
@@ -184,7 +188,7 @@ GEM
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.2.0)
- google-cloud-storage (1.35.0)
+ google-cloud-storage (1.36.1)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@@ -192,8 +196,8 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
- googleauth (1.1.0)
- faraday (>= 0.17.3, < 2.0)
+ googleauth (1.1.2)
+ faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
@@ -204,15 +208,15 @@ GEM
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
- i18n (1.8.11)
+ i18n (1.10.0)
concurrent-ruby (~> 1.0)
- jmespath (1.4.0)
+ jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
- mime-types-data (3.2021.1115)
+ mime-types-data (3.2022.0105)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.15.0)
@@ -244,9 +248,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
- signet (0.16.0)
+ signet (0.16.1)
addressable (~> 2.8)
- faraday (>= 0.17.3, < 2.0)
+ faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@@ -267,7 +271,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.8)
+ unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
@@ -285,7 +289,7 @@ GEM
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
- zeitwerk (2.5.1)
+ zeitwerk (2.5.4)
PLATFORMS
ruby
@@ -299,4 +303,4 @@ DEPENDENCIES
xcode-install
BUNDLED WITH
- 2.2.32
+ 2.3.9
diff --git a/Podfile b/Podfile
index 07fe178d6..fe7524f70 100644
--- a/Podfile
+++ b/Podfile
@@ -13,7 +13,7 @@ use_frameworks!
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
#
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
-$matrixSDKVersion = '= 0.22.6'
+$matrixSDKVersion = '= 0.23.0'
# $matrixSDKVersion = :local
# $matrixSDKVersion = { :branch => 'develop'}
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
diff --git a/Podfile.lock b/Podfile.lock
index 920b90078..9b821e6f6 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -45,7 +45,7 @@ PODS:
- GBDeviceInfo/Core (= 6.6.0)
- GBDeviceInfo/Core (6.6.0)
- GZIP (1.3.0)
- - Introspect (0.1.3)
+ - Introspect (0.1.4)
- JitsiMeetSDK (3.10.2)
- KeychainAccess (4.2.2)
- KituraContracts (1.2.1):
@@ -180,7 +180,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
AnalyticsEvents:
- :commit: 0101e4fd25ded5fb2cba8a9119cb061e36296369
+ :commit: f37a2f243270bffdcddfa5cbaeb4379e0db581c2
:git: https://github.com/matrix-org/matrix-analytics-events.git
SPEC CHECKSUMS:
@@ -199,7 +199,7 @@ SPEC CHECKSUMS:
FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2
GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec
GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3
- Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e
+ Introspect: b62c4dd2063072327c21d618ef2bedc3c87bc366
JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b
@@ -227,4 +227,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 16aaf5e59ec902619fbfd799939f044728a92ab7
-COCOAPODS: 1.11.2
+COCOAPODS: 1.11.3
diff --git a/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/Contents.json
new file mode 100644
index 000000000..f9e54fde0
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "filename" : "coach_mark.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "coach_mark@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "coach_mark@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark.png b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark.png
new file mode 100644
index 000000000..c77c890f8
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark.png differ
diff --git a/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark@2x.png b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark@2x.png
new file mode 100644
index 000000000..0e28b64fa
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark@3x.png b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark@3x.png
new file mode 100644
index 000000000..7d58463bd
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Common/coach_mark.imageset/coach_mark@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_camera.imageset/Contents.json b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_camera.imageset/Contents.json
new file mode 100644
index 000000000..98829d4af
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_camera.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "onboarding_avatar_camera.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_camera.imageset/onboarding_avatar_camera.svg b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_camera.imageset/onboarding_avatar_camera.svg
new file mode 100644
index 000000000..50893f857
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_camera.imageset/onboarding_avatar_camera.svg
@@ -0,0 +1,4 @@
+
diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_edit.imageset/Contents.json b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_edit.imageset/Contents.json
new file mode 100644
index 000000000..7026b6d89
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_edit.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "onboarding_avatar_edit.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_edit.imageset/onboarding_avatar_edit.svg b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_edit.imageset/onboarding_avatar_edit.svg
new file mode 100644
index 000000000..f17013d91
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Onboarding/onboarding_avatar_edit.imageset/onboarding_avatar_edit.svg
@@ -0,0 +1,4 @@
+
diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json
new file mode 100644
index 000000000..e312c8132
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "filename" : "live_location_icon.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "live_location_icon@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "live_location_icon@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon.png b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon.png
new file mode 100644
index 000000000..15d2d7b11
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@2x.png
new file mode 100644
index 000000000..cc3a8fb4e
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@3x.png
new file mode 100644
index 000000000..59282b8f1
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/Contents.json
new file mode 100644
index 000000000..de2178f44
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "dark-theme-no-mentions.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "original"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/dark-theme-no-mentions.svg b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/dark-theme-no-mentions.svg
new file mode 100644
index 000000000..ae6aa847b
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/dark-theme-no-mentions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/Contents.json
new file mode 100644
index 000000000..9d412b77e
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "light-theme-no-mentions.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "original"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/light-theme-no-mentions.svg b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/light-theme-no-mentions.svg
new file mode 100644
index 000000000..f8468cbd2
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/light-theme-no-mentions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/Contents.json
new file mode 100644
index 000000000..dd53ab236
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "light-and-dark-theme-mentions.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "original"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/light-and-dark-theme-mentions.svg b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/light-and-dark-theme-mentions.svg
new file mode 100644
index 000000000..2bf8d2125
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/light-and-dark-theme-mentions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/Contents.json
new file mode 100644
index 000000000..e855c865c
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "filename" : "room_access_info_header_icon.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "room_access_info_header_icon@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "room_access_info_header_icon@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon.png b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon.png
new file mode 100644
index 000000000..53a2a144c
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon@2x.png b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon@2x.png
new file mode 100644
index 000000000..4ebadac91
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon@3x.png b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon@3x.png
new file mode 100644
index 000000000..4b2a4d32f
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/room_access_info_header_icon.imageset/room_access_info_header_icon@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/Contents.json
similarity index 67%
rename from Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/Contents.json
rename to Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/Contents.json
index 9b395447b..cf8bba813 100644
--- a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/Contents.json
+++ b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/Contents.json
@@ -1,17 +1,17 @@
{
"images" : [
{
- "filename" : "space_home_icon.png",
+ "filename" : "space_add_room.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "space_home_icon@2x.png",
+ "filename" : "space_add_room@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
- "filename" : "space_home_icon@3x.png",
+ "filename" : "space_add_room@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room.png b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room.png
new file mode 100644
index 000000000..451a80229
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@2x.png
new file mode 100644
index 000000000..b385451f0
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@3x.png
new file mode 100644
index 000000000..de967119a
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_add_room.imageset/space_add_room@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/Contents.json
new file mode 100644
index 000000000..eb54967ab
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "space_creation_camera.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "space_creation_camera@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "space_creation_camera@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera.png
new file mode 100644
index 000000000..65264a038
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera@2x.png
new file mode 100644
index 000000000..f87bc29e8
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera@3x.png
new file mode 100644
index 000000000..c593c5bf5
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_camera.imageset/space_creation_camera@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/Contents.json
new file mode 100644
index 000000000..bb2b6cc6f
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "space_creation_private.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "space_creation_private@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "space_creation_private@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private.png
new file mode 100644
index 000000000..a77ac3c62
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private@2x.png
new file mode 100644
index 000000000..581625eaf
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private@3x.png
new file mode 100644
index 000000000..9441bbbbc
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_private.imageset/space_creation_private@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/Contents.json
new file mode 100644
index 000000000..d5cfa2de7
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "space_creation_public.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "space_creation_public@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "space_creation_public@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public.png
new file mode 100644
index 000000000..dce0fb56a
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public@2x.png
new file mode 100644
index 000000000..38caf7d4e
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public@3x.png
new file mode 100644
index 000000000..3ff76fca5
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_creation_public.imageset/space_creation_public@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon.png
deleted file mode 100644
index 75af27227..000000000
Binary files a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon@2x.png
deleted file mode 100644
index 7c4243805..000000000
Binary files a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon@2x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon@3x.png
deleted file mode 100644
index 1076124e2..000000000
Binary files a/Riot/Assets/Images.xcassets/Spaces/space_home_icon.imageset/space_home_icon@3x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/Contents.json
new file mode 100644
index 000000000..4513c28c2
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "space_home_icon_dark.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "space_home_icon_dark@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "space_home_icon_dark@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark.png
new file mode 100644
index 000000000..8f122d2c9
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark@2x.png
new file mode 100644
index 000000000..cf8c23942
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark@3x.png
new file mode 100644
index 000000000..5960d13a5
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_dark.imageset/space_home_icon_dark@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/Contents.json
new file mode 100644
index 000000000..97d17d9bb
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "space_home_icon_light.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "space_home_icon_light@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "space_home_icon_light@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light.png
new file mode 100644
index 000000000..fb671b141
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light@2x.png
new file mode 100644
index 000000000..d6396bb1a
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light@3x.png
new file mode 100644
index 000000000..2d527378c
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_home_icon_light.imageset/space_home_icon_light@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/Contents.json
new file mode 100644
index 000000000..d428b7ae6
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "filename" : "space_invite_user.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "space_invite_user@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "space_invite_user@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user.png b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user.png
new file mode 100644
index 000000000..672632139
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@2x.png
new file mode 100644
index 000000000..cfda74c5b
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@3x.png
new file mode 100644
index 000000000..fcd432e87
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_invite_user.imageset/space_invite_user@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/Contents.json
new file mode 100644
index 000000000..943055f00
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "filename" : "space_menu_plus_icon.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "space_menu_plus_icon@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "space_menu_plus_icon@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon.png b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon.png
new file mode 100644
index 000000000..cfc87199c
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@2x.png
new file mode 100644
index 000000000..c3cc1ee60
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@3x.png
new file mode 100644
index 000000000..bf5489cc2
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_menu_plus_icon.imageset/space_menu_plus_icon@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/Contents.json
new file mode 100644
index 000000000..f5861fe3f
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "spaces_add_space_dark.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "spaces_add_space_dark@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "spaces_add_space_dark@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark.png b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark.png
new file mode 100644
index 000000000..2e8b66fd0
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark@2x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark@2x.png
new file mode 100644
index 000000000..dfc11fc8f
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark@3x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark@3x.png
new file mode 100644
index 000000000..9711a05a4
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_dark.imageset/spaces_add_space_dark@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/Contents.json
new file mode 100644
index 000000000..3003074fa
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "spaces_add_space_light.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "spaces_add_space_light@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "spaces_add_space_light@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light.png b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light.png
new file mode 100644
index 000000000..948a2b3bd
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light@2x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light@2x.png
new file mode 100644
index 000000000..55ce9ba21
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light@3x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light@3x.png
new file mode 100644
index 000000000..1db00fbb1
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_add_space_light.imageset/spaces_add_space_light@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/Contents.json
new file mode 100644
index 000000000..d6068239b
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "spaces_invite_users.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "spaces_invite_users@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "spaces_invite_users@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users.png b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users.png
new file mode 100644
index 000000000..5c6b4401b
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users@2x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users@2x.png
new file mode 100644
index 000000000..4a36ffbe2
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users@3x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users@3x.png
new file mode 100644
index 000000000..2568a3280
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_invite_users.imageset/spaces_invite_users@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/Contents.json
new file mode 100644
index 000000000..75f10b9f4
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "spaces_modal_back.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "spaces_modal_back@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "spaces_modal_back@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back.png b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back.png
new file mode 100644
index 000000000..b9c92417e
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back@2x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back@2x.png
new file mode 100644
index 000000000..d299195d2
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back@3x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back@3x.png
new file mode 100644
index 000000000..3a869e88e
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_back.imageset/spaces_modal_back@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/Contents.json
new file mode 100644
index 000000000..93d85da8a
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "spaces_modal_close.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "spaces_modal_close@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "spaces_modal_close@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close.png b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close.png
new file mode 100644
index 000000000..751a9bd59
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close@2x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close@2x.png
new file mode 100644
index 000000000..365ac0fac
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close@3x.png b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close@3x.png
new file mode 100644
index 000000000..33d3f90da
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/spaces_modal_close.imageset/spaces_modal_close@3x.png differ
diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings
index d55b74231..51087b0b1 100644
--- a/Riot/Assets/en.lproj/Untranslated.strings
+++ b/Riot/Assets/en.lproj/Untranslated.strings
@@ -16,8 +16,23 @@
/** These strings will be ignored by Weblate. Useful for WIP **/
-// MARK: Onboarding Personalisation WIP
+// MARK: Onboarding Personalization WIP
"onboarding_congratulations_title" = "Congratulations!";
-"onboarding_congratulations_message" = "Your account\n%@\nhas been created.";
-"onboarding_congratulations_personalise_button" = "Personalise profile";
+"onboarding_congratulations_message" = "Your account %@ has been created.";
+"onboarding_congratulations_personalize_button" = "Personalise profile";
"onboarding_congratulations_home_button" = "Take me home";
+
+"onboarding_personalization_save" = "Save and continue";
+"onboarding_personalization_skip" = "Skip this step";
+
+"onboarding_display_name_title" = "Choose a display name";
+"onboarding_display_name_message" = "This will be shown when you send messages.";
+"onboarding_display_name_placeholder" = "Display Name";
+"onboarding_display_name_hint" = "You can change this later";
+"onboarding_display_name_max_length" = "Your display name must be less than 256 characters";
+
+"onboarding_avatar_title" = "Add a profile picture";
+"onboarding_avatar_message" = "You can change this anytime.";
+"onboarding_avatar_accessibility_label" = "Profile picture";
+
+"image_picker_action_files" = "Choose from files";
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 9d6727dbb..fa9055e44 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -57,7 +57,6 @@
"rename" = "Rename";
"collapse" = "collapse";
"send_to" = "Send to %@";
-"sending" = "Sending";
"close" = "Close";
"skip" = "Skip";
"joined" = "Joined";
@@ -66,7 +65,21 @@
"less" = "Less";
"open" = "Open";
"done" = "Done";
+"private" = "Private";
+"public" = "Public";
+"stop" = "Stop";
+"new_word" = "New";
+"existing" = "Existing";
+"add" = "Add";
"ok" = "OK";
+"error" = "Error";
+"suggest" = "Suggest";
+"edit" = "Edit";
+
+// Activities
+"loading" = "Loading";
+"sending" = "Sending";
+"saving" = "Saving";
// Call Bar
"callbar_only_single_active" = "Tap to return to the call (%@)";
@@ -297,6 +310,7 @@ Tap the + to start adding people.";
"room_participants_remove_third_party_invite_prompt_msg" = "Are you sure you want to revoke this invite?";
"room_participants_invite_prompt_title" = "Confirmation";
"room_participants_invite_prompt_msg" = "Are you sure you want to invite %@ to this chat?";
+"room_participants_invite_prompt_to_msg" = "Are you sure you want to invite %@ to %@?";
"room_participants_filter_room_members" = "Filter room members";
"room_participants_filter_room_members_for_dm" = "Filter members";
"room_participants_invite_another_user" = "Search / invite by User ID, Name or email";
@@ -809,8 +823,11 @@ Tap the + to start adding people.";
"room_details_access_section_anyone"="Anyone who knows the room's link, including guests";
"room_details_access_section_anyone_for_dm"="Anyone who knows the link, including guests";
"room_details_access_section_no_address_warning" = "To link to a room it must have an address";
+"room_details_access_row_title" = "Access";
+"room_details_promote_room_title" = "Promote room";
"room_details_access_section_directory_toggle"="List this room in room directory";
"room_details_access_section_directory_toggle_for_dm"="List in room directory";
+"room_details_promote_room_suggest_title" = "Suggest to space members";
"room_details_history_section"="Who can read history?";
"room_details_history_section_anyone"="Anyone";
"room_details_history_section_members_only"="Members only (since the point in time of selecting this option)";
@@ -861,6 +878,32 @@ Tap the + to start adding people.";
"room_details_copy_room_address" = "Copy Room Address";
"room_details_copy_room_url" = "Copy Room URL";
+// Room Access Settings
+"room_access_settings_screen_nav_title" = "Room access";
+"room_access_settings_screen_title" = "Who can access this room?";
+"room_access_settings_screen_message" = "Decide who can find and join %@.";
+"room_access_settings_screen_private_message" = "Only invited people can find and join.";
+"room_access_settings_screen_restricted_message" = "Let anyone in a space find and join.\nYou’ll be asked to confirm which spaces.";
+"room_access_settings_screen_upgrade_required" = "Upgrade required";
+"room_access_settings_screen_edit_spaces" = "Edit spaces";
+"room_access_settings_screen_public_message" = "Anyone can find and join.";
+
+"room_access_settings_screen_upgrade_alert_title" = "Upgrade room";
+"room_access_settings_screen_upgrade_alert_message" = "Anyone in Space name will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.\n\nPlease note upgrading will make a new version of the room. All current messages will stay in this archived room.";
+"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Automatically invite members to new room";
+"room_access_settings_screen_upgrade_alert_upgrade_button" = "Upgrade";
+"room_access_settings_screen_upgrade_alert_upgrading" = "Upgrading room";
+"room_access_settings_screen_setting_room_access" = "Setting room access";
+
+"room_access_space_chooser_known_spaces_section" = "Spaces you know containing %@";
+"room_access_space_chooser_other_spaces_section" = "Other spaces or rooms";
+"room_access_space_chooser_other_spaces_section_info" = "These are likely things other admins of %@ are a part of.";
+
+// Room suggestion Settings
+"room_suggestion_settings_screen_nav_title" = "Suggest room";
+"room_suggestion_settings_screen_title" = "Make a room suggested in a space";
+"room_suggestion_settings_screen_message" = "Suggested rooms are promoted to space members as good ones to join.";
+
// Room Notification Settings
"room_notifs_settings_notify_me_for" = "Notify me for";
"room_notifs_settings_all_messages" = "All Messages";
@@ -1700,20 +1743,27 @@ Tap the + to start adding people.";
// MARK: - Create Room
"create_room_title" = "New Room";
-"create_room_section_header_name" = "Room name";
+"create_room_section_header_name" = "NAME";
"create_room_placeholder_name" = "Name";
-"create_room_section_header_topic" = "Room topic (optional)";
-"create_room_placeholder_topic" = "Topic";
-"create_room_section_header_encryption" = "Room encryption";
+"create_room_section_header_topic" = "TOPIC (OPTIONAL)";
+"create_room_placeholder_topic" = "What is this room about?";
+"create_room_section_header_encryption" = "ENCRYPTION";
"create_room_enable_encryption" = "Enable Encryption";
"create_room_section_footer_encryption" = "Encryption can’t be disabled afterwards.";
-"create_room_section_header_type" = "Room type";
-"create_room_type_private" = "Private Room";
-"create_room_type_public" = "Public Room";
-"create_room_section_footer_type" = "People join a private room only with the room invitation.";
-"create_room_show_in_directory" = "Show the room in the directory";
-"create_room_section_header_address" = "Room address";
+"create_room_section_header_type" = "WHO CAN ACCESS";
+"create_room_type_private" = "Private Room (invite only)";
+"create_room_type_restricted" = "Space members";
+"create_room_type_public" = "Public Room (anyone)";
+"create_room_section_footer_type_private" = "Only people invited can find and join.";
+"create_room_section_footer_type_restricted" = "Anyone in Space name can find and join.";
+"create_room_section_footer_type_public" = "Only people invited can find and join, not just people in Space name.";
+"create_room_promotion_header" = "PROMOTION";
+"create_room_show_in_directory" = "Show in room directory";
+"create_room_show_in_directory_footer" = "This will help people find and join.";
+"create_room_section_header_address" = "ADDRESS";
"create_room_placeholder_address" = "#testroom:matrix.org";
+"create_room_suggest_room" = "Suggest to space members";
+"create_room_suggest_room_footer" = "Suggested rooms are promoted to space members as good to join.";
// MARK: - Room Info
@@ -1759,6 +1809,12 @@ Tap the + to start adding people.";
"invite_friends_action" = "Invite friends to %@";
"invite_friends_share_text" = "Hey, talk to me on %@: %@";
+// MARK: - Share invite link
+
+"share_invite_link_action" = "Share invite link";
+"share_invite_link_room_text" = "Hey, join this room on %@";
+"share_invite_link_space_text" = "Hey, join this space on %@";
+
// Mark: - Room avatar view
"room_avatar_view_accessibility_label" = "avatar";
@@ -1781,6 +1837,15 @@ Tap the + to start adding people.";
"room_intro_cell_information_dm_sentence2" = "Only the two of you are in this conversation, no one else can join.";
"room_intro_cell_information_multiple_dm_sentence2" = "Only you are in this conversation, unless any of you invites someone to join.";
+// Mark: - Room invite
+
+"room_invite_to_space_option_title" = "To %@";
+"room_invite_to_space_option_detail" = "They can explore %@, but won’t be a member of %@.";
+"room_invite_to_room_option_title" = "To just this room";
+"room_invite_to_room_option_detail" = "They won’t be a part of %@.";
+"room_invite_not_enough_permission" = "You do not have permission to invite people to this room";
+"space_invite_not_enough_permission" = "You do not have permission to invite people to this space";
+
// Mark: - Spaces
"space_feature_unavailable_title" = "Spaces aren’t here yet";
@@ -1793,6 +1858,8 @@ Tap the + to start adding people.";
"space_beta_announce_information" = "Spaces are a new way to group rooms and people. They’re not on iOS yet, but you can use them now on Web and Desktop.";
"spaces_home_space_title" = "Home";
+"spaces_add_space_title" = "Create space";
+"spaces_create_space_title" = "Create a space";
"spaces_left_panel_title" = "Spaces";
"leave_space_title" = "Leave %@";
"leave_space_message" = "Are you sure you want to leave %@? Do you also want to leave all rooms and spaces of this space?";
@@ -1801,6 +1868,8 @@ Tap the + to start adding people.";
"leave_space_and_all_rooms_action" = "Leave all rooms and spaces";
"spaces_explore_rooms" = "Explore rooms";
"spaces_suggested_room" = "Suggested";
+"spaces_explore_rooms_room_number" = "%@ rooms";
+"spaces_explore_rooms_one_room" = "1 room";
"space_tag" = "space";
"spaces_empty_space_title" = "This space has no rooms (yet)";
"spaces_empty_space_detail" = "Some rooms may be hidden because they’re private and you need an invite.";
@@ -1816,7 +1885,78 @@ Tap the + to start adding people.";
"space_home_show_all_rooms" = "Show all rooms";
"space_private_join_rule" = "Private space";
+"space_private_join_rule_detail" = "Invite only, best for yourself or teams";
"space_public_join_rule" = "Public space";
+"spaces_invite_people" = "Invite people";
+"spaces_add_room" = "Add room";
+"spaces_add_space" = "Add space";
+"space_public_join_rule_detail" = "Open to anyone, best for communities";
+
+"space_topic" = "Description";
+
+"space_settings_access_section" = "Who can access this space?";
+"space_settings_update_failed_message" = "Failed to update space settings. Do you want to retry?";
+"space_settings_current_address_message" = "Your space is viewable at\n%@";
+
+// Mark: - Space Creation
+
+"spaces_creation_hint" = "Spaces are a new way to group rooms and people.";
+"spaces_creation_visibility_title" = "What type of space do you want to create?";
+"spaces_creation_visibility_message" = "To join an existing space, you need an invite.";
+"spaces_creation_footer" = "You can change this later";
+"spaces_creation_settings_message" = "Add some details to help it stand out. You can change these at any point.";
+"spaces_creation_address" = "Address";
+"spaces_creation_empty_room_name_error" = "Name required";
+"spaces_creation_address_default_message" = "Your space will be viewable at\n%@";
+"spaces_creation_address_invalid_characters" = "%@\nhas invalid characters";
+"spaces_creation_address_already_exists" = "%@\nalready exists";
+"spaces_creation_public_space_title" = "Your public space";
+"spaces_creation_private_space_title" = "Your private space";
+"spaces_creation_cancel_title" = "Stop creating a space?";
+"spaces_creation_cancel_message" = "Your progress will be lost.";
+
+"spaces_creation_new_rooms_title" = "What are some discussions you’ll have?";
+"spaces_creation_new_rooms_message" = "We’ll create a room for each one.";
+"spaces_creation_new_rooms_room_name_title" = "Room name";
+"spaces_creation_new_rooms_general" = "General";
+"spaces_creation_new_rooms_random" = "Random";
+"spaces_creation_new_rooms_support" = "Support";
+
+"spaces_creation_email_invites_title" = "Invite your team";
+"spaces_creation_email_invites_message" = "You can invite them later too.";
+"spaces_creation_email_invites_email_title" = "Email";
+
+"spaces_creation_sharing_type_title" = "Who are you working with?";
+"spaces_creation_sharing_type_message" = "Make sure the right people have access %@. You can change this later.";
+"spaces_creation_sharing_type_just_me_title" = "Just me";
+"spaces_creation_sharing_type_just_me_detail" = "A private space to organise your rooms";
+"spaces_creation_sharing_type_me_and_teammates_title" = "Me and teammates";
+"spaces_creation_sharing_type_me_and_teammates_detail" = "A private space for you & your teammates";
+
+"spaces_creation_add_rooms_title" = "What do you want to add?";
+"spaces_creation_add_rooms_message" = "As this space is just for you, no one will be informed. You can add more later.";
+
+"spaces_creation_invite_by_username" = "Invite by username";
+"spaces_creation_invite_by_username_title" = "Invite your team";
+"spaces_creation_invite_by_username_message" = "You can invite them later too.";
+
+"spaces_creation_post_process_creating_space" = "Creating space";
+"spaces_creation_post_process_creating_space_task" = "Creating %@";
+"spaces_creation_post_process_uploading_avatar" = "Uploading avatar";
+"spaces_creation_post_process_creating_room" = "Creating %@";
+"spaces_creation_post_process_adding_rooms" = "Adding %@ rooms";
+"spaces_creation_post_process_inviting_users" = "Inviting %@ users";
+
+"spaces_creation_in_spacename" = "in %@";
+"spaces_creation_in_spacename_plus_one" = "in %@ + 1 space";
+"spaces_creation_in_spacename_plus_many" = "in %@ + %@ spaces";
+"spaces_creation_in_many_spaces" = "in %@ spaces";
+"spaces_creation_in_one_space" = "in 1 space";
+
+"spaces_invite_people" = "Invite people";
+"spaces_add_room" = "Add room";
+"spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space.";
+"spaces_add_space" = "Add space";
// Mark: Avatar
@@ -1836,6 +1976,7 @@ Tap the + to start adding people.";
"side_menu_action_help" = "Help";
"side_menu_action_feedback" = "Feedback";
"side_menu_app_version" = "Version %@";
+"side_menu_coach_message" = "Swipe right or tap to see all rooms";
// Mark: - Voice Messages
@@ -1952,6 +2093,11 @@ Tap the + to start adding people.";
"location_sharing_settings_toggle_title" = "Enable location sharing";
+// MARK: Live location sharing
+
+"live_location_sharing_banner_title" = "Live location enabled";
+"live_location_sharing_banner_stop" = "Stop";
+
// MARK: - MatrixKit
diff --git a/Riot/Categories/Bundle.swift b/Riot/Categories/Bundle.swift
index 055d9c64c..5b3430154 100644
--- a/Riot/Categories/Bundle.swift
+++ b/Riot/Categories/Bundle.swift
@@ -30,4 +30,9 @@ public extension Bundle {
}
return bundle
}
+
+ /// Whether or not the bundle is the RiotShareExtension.
+ var isShareExtension: Bool {
+ bundleURL.lastPathComponent.contains("RiotShareExtension.appex")
+ }
}
diff --git a/Riot/Categories/MXSession+Riot.m b/Riot/Categories/MXSession+Riot.m
index 97a74e5b2..aff1ec0e1 100644
--- a/Riot/Categories/MXSession+Riot.m
+++ b/Riot/Categories/MXSession+Riot.m
@@ -59,7 +59,7 @@
success:(void (^)(BOOL canEnableE2E))success
failure:(void (^)(NSError *error))failure;
{
- if ([self vc_homeserverConfiguration].isE2EEByDefaultEnabled)
+ if ([self vc_homeserverConfiguration].encryption.isE2EEByDefaultEnabled)
{
return [self canEnableE2EByDefaultInNewRoomWithUsers:userIds success:success failure:failure];
}
diff --git a/Riot/Categories/UIButton.swift b/Riot/Categories/UIButton.swift
index 6f896bb58..195a2c244 100644
--- a/Riot/Categories/UIButton.swift
+++ b/Riot/Categories/UIButton.swift
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import Foundation
+import UIKit
extension UIButton {
@@ -51,4 +51,10 @@ extension UIButton {
self.titleLabel?.adjustsFontForContentSizeCategory = newValue
}
}
+
+ /// Set title font and enable Dynamic Type support
+ func vc_setTitleFont(_ font: UIFont) {
+ self.vc_adjustsFontForContentSizeCategory = true
+ self.titleLabel?.font = font
+ }
}
diff --git a/Riot/Categories/UIViewController.swift b/Riot/Categories/UIViewController.swift
index e1c16760e..1b2628c75 100644
--- a/Riot/Categories/UIViewController.swift
+++ b/Riot/Categories/UIViewController.swift
@@ -44,28 +44,50 @@ extension UIViewController {
/// - Parameters:
/// - viewController: The child view controller to add.
/// - view: The view on which to add the child view controller view.
- func vc_addChildViewController(viewController: UIViewController, onView view: UIView) {
+ /// - animated: true to add a fade in animation
+ func vc_addChildViewController(viewController: UIViewController, onView view: UIView, animated: Bool = false) {
self.addChild(viewController)
viewController.view.frame = view.bounds
+ if animated {
+ viewController.view.alpha = 0
+ }
view.vc_addSubViewMatchingParent(viewController.view)
+ if animated {
+ UIView.animate(withDuration: 0.2) {
+ viewController.view.alpha = 1
+ }
+ }
viewController.didMove(toParent: self)
}
/// Remove a child view controller from current view controller.
///
- /// - Parameter viewController: The child view controller to remove.
- func vc_removeChildViewController(viewController: UIViewController) {
+ /// - Parameters:
+ /// - viewController: The child view controller to remove.
+ /// - animated: true to add a fade out animation
+ func vc_removeChildViewController(viewController: UIViewController, animated: Bool = false) {
viewController.willMove(toParent: nil)
- viewController.view.removeFromSuperview()
+ if animated {
+ UIView.animate(withDuration: 0.2) {
+ viewController.view.alpha = 0
+ } completion: { finished in
+ viewController.view.removeFromSuperview()
+ viewController.view.alpha = 1
+ }
+ } else {
+ viewController.view.removeFromSuperview()
+ }
viewController.removeFromParent()
}
/// Remove current view controller from parent.
- func vc_removeFromParent() {
- self.vc_removeChildViewController(viewController: self)
+ ///
+ /// - Parameter animated: true to add a fade out animation
+ func vc_removeFromParent(animated: Bool = false) {
+ self.vc_removeChildViewController(viewController: self, animated: animated)
}
/// Adds a floating action button to the bottom-right of the page.
@@ -115,4 +137,18 @@ extension UIViewController {
// set split view display mode button as left bar button item
self.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
}
+
+ /// Set the view controller to be displayed in fullscreen modal presentation style on any iOS version.
+ ///
+ /// - Parameters:
+ /// - isFullScreen: whether view controller should be displayed full screen
+ /// - Returns: the view controller
+ @discardableResult
+ func vc_setModalFullScreen(_ isFullScreen: Bool) -> UIViewController {
+ if #available(iOS 13.0, *) {
+ self.modalPresentationStyle = isFullScreen ? .fullScreen : .automatic
+ }
+
+ return self
+ }
}
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index 65511e31c..469b4b1e7 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -60,6 +60,7 @@ internal class Asset: NSObject {
internal static let checkmark = ImageAsset(name: "checkmark")
internal static let chevron = ImageAsset(name: "chevron")
internal static let closeButton = ImageAsset(name: "close_button")
+ internal static let coachMark = ImageAsset(name: "coach_mark")
internal static let disclosureIcon = ImageAsset(name: "disclosure_icon")
internal static let errorIcon = ImageAsset(name: "error_icon")
internal static let faceidIcon = ImageAsset(name: "faceid_icon")
@@ -123,6 +124,8 @@ internal class Asset: NSObject {
internal static let onboardingSplashScreenPage3Dark = ImageAsset(name: "OnboardingSplashScreenPage3Dark")
internal static let onboardingSplashScreenPage4 = ImageAsset(name: "OnboardingSplashScreenPage4")
internal static let onboardingSplashScreenPage4Dark = ImageAsset(name: "OnboardingSplashScreenPage4Dark")
+ internal static let onboardingAvatarCamera = ImageAsset(name: "onboarding_avatar_camera")
+ internal static let onboardingAvatarEdit = ImageAsset(name: "onboarding_avatar_edit")
internal static let onboardingCongratulationsIcon = ImageAsset(name: "onboarding_congratulations_icon")
internal static let onboardingUseCaseCommunity = ImageAsset(name: "onboarding_use_case_community")
internal static let onboardingUseCaseCommunityDark = ImageAsset(name: "onboarding_use_case_community_dark")
@@ -168,6 +171,7 @@ internal class Asset: NSObject {
internal static let videoCall = ImageAsset(name: "video_call")
internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon")
internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon")
+ internal static let liveLocationIcon = ImageAsset(name: "live_location_icon")
internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon")
internal static let locationShareIcon = ImageAsset(name: "location_share_icon")
internal static let locationUserMarker = ImageAsset(name: "location_user_marker")
@@ -183,6 +187,9 @@ internal class Asset: NSObject {
internal static let threadsFilter = ImageAsset(name: "threads_filter")
internal static let threadsFilterApplied = ImageAsset(name: "threads_filter_applied")
internal static let threadsIcon = ImageAsset(name: "threads_icon")
+ internal static let threadsIconGrayDotDark = ImageAsset(name: "threads_icon_gray_dot_dark")
+ internal static let threadsIconGrayDotLight = ImageAsset(name: "threads_icon_gray_dot_light")
+ internal static let threadsIconRedDot = ImageAsset(name: "threads_icon_red_dot")
internal static let urlPreviewClose = ImageAsset(name: "url_preview_close")
internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark")
internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient")
@@ -207,6 +214,7 @@ internal class Asset: NSObject {
internal static let modIcon = ImageAsset(name: "mod_icon")
internal static let moreReactions = ImageAsset(name: "more_reactions")
internal static let notifications = ImageAsset(name: "notifications")
+ internal static let roomAccessInfoHeaderIcon = ImageAsset(name: "room_access_info_header_icon")
internal static let scrollup = ImageAsset(name: "scrollup")
internal static let roomsEmptyScreenArtwork = ImageAsset(name: "rooms_empty_screen_artwork")
internal static let roomsEmptyScreenArtworkDark = ImageAsset(name: "rooms_empty_screen_artwork_dark")
@@ -232,15 +240,27 @@ internal class Asset: NSObject {
internal static let sideMenuNotifIcon = ImageAsset(name: "side_menu_notif_icon")
internal static let featureUnavaibleArtwork = ImageAsset(name: "feature_unavaible_artwork")
internal static let featureUnavaibleArtworkDark = ImageAsset(name: "feature_unavaible_artwork_dark")
- internal static let spaceHomeIcon = ImageAsset(name: "space_home_icon")
+ internal static let spaceAddRoom = ImageAsset(name: "space_add_room")
+ internal static let spaceCreationCamera = ImageAsset(name: "space_creation_camera")
+ internal static let spaceCreationPrivate = ImageAsset(name: "space_creation_private")
+ internal static let spaceCreationPublic = ImageAsset(name: "space_creation_public")
+ internal static let spaceHomeIconDark = ImageAsset(name: "space_home_icon_dark")
+ internal static let spaceHomeIconLight = ImageAsset(name: "space_home_icon_light")
+ internal static let spaceInviteUser = ImageAsset(name: "space_invite_user")
internal static let spaceMenuClose = ImageAsset(name: "space_menu_close")
internal static let spaceMenuLeave = ImageAsset(name: "space_menu_leave")
internal static let spaceMenuMembers = ImageAsset(name: "space_menu_members")
+ internal static let spaceMenuPlusIcon = ImageAsset(name: "space_menu_plus_icon")
internal static let spaceMenuRooms = ImageAsset(name: "space_menu_rooms")
internal static let spacePrivateIcon = ImageAsset(name: "space_private_icon")
internal static let spaceRoomIcon = ImageAsset(name: "space_room_icon")
internal static let spaceTypeIcon = ImageAsset(name: "space_type_icon")
internal static let spaceUserIcon = ImageAsset(name: "space_user_icon")
+ internal static let spacesAddSpaceDark = ImageAsset(name: "spaces_add_space_dark")
+ internal static let spacesAddSpaceLight = ImageAsset(name: "spaces_add_space_light")
+ internal static let spacesInviteUsers = ImageAsset(name: "spaces_invite_users")
+ internal static let spacesModalBack = ImageAsset(name: "spaces_modal_back")
+ internal static let spacesModalClose = ImageAsset(name: "spaces_modal_close")
internal static let spacesMore = ImageAsset(name: "spaces_more")
internal static let tabFavourites = ImageAsset(name: "tab_favourites")
internal static let tabGroups = ImageAsset(name: "tab_groups")
diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift
index 2a92564a0..ca9af8f55 100644
--- a/Riot/Generated/Storyboards.swift
+++ b/Riot/Generated/Storyboards.swift
@@ -147,6 +147,11 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType(storyboard: MajorUpdateViewController.self)
}
+ internal enum OptionListViewController: StoryboardType {
+ internal static let storyboardName = "OptionListViewController"
+
+ internal static let initialScene = InitialSceneType(storyboard: OptionListViewController.self)
+ }
internal enum QRCodeReaderViewController: StoryboardType {
internal static let storyboardName = "QRCodeReaderViewController"
@@ -274,6 +279,11 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType(storyboard: SpaceMenuViewController.self)
}
+ internal enum SpaceRoomPreviewViewController: StoryboardType {
+ internal static let storyboardName = "SpaceRoomPreviewViewController"
+
+ internal static let initialScene = InitialSceneType(storyboard: SpaceRoomPreviewViewController.self)
+ }
internal enum TemplateScreenViewController: StoryboardType {
internal static let storyboardName = "TemplateScreenViewController"
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index cf2c25d68..8a751f0d7 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -111,6 +111,10 @@ public class VectorL10n: NSObject {
public static func activeCallDetails(_ p1: String) -> String {
return VectorL10n.tr("Vector", "active_call_details", p1)
}
+ /// Add
+ public static var add: String {
+ return VectorL10n.tr("Vector", "add")
+ }
/// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices.
public static func analyticsPromptMessageNewUser(_ p1: String) -> String {
return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1)
@@ -923,54 +927,82 @@ public class VectorL10n: NSObject {
public static var createRoomPlaceholderName: String {
return VectorL10n.tr("Vector", "create_room_placeholder_name")
}
- /// Topic
+ /// What is this room about?
public static var createRoomPlaceholderTopic: String {
return VectorL10n.tr("Vector", "create_room_placeholder_topic")
}
+ /// PROMOTION
+ public static var createRoomPromotionHeader: String {
+ return VectorL10n.tr("Vector", "create_room_promotion_header")
+ }
/// Encryption can’t be disabled afterwards.
public static var createRoomSectionFooterEncryption: String {
return VectorL10n.tr("Vector", "create_room_section_footer_encryption")
}
- /// People join a private room only with the room invitation.
- public static var createRoomSectionFooterType: String {
- return VectorL10n.tr("Vector", "create_room_section_footer_type")
+ /// Only people invited can find and join.
+ public static var createRoomSectionFooterTypePrivate: String {
+ return VectorL10n.tr("Vector", "create_room_section_footer_type_private")
}
- /// Room address
+ /// Only people invited can find and join, not just people in Space name.
+ public static var createRoomSectionFooterTypePublic: String {
+ return VectorL10n.tr("Vector", "create_room_section_footer_type_public")
+ }
+ /// Anyone in Space name can find and join.
+ public static var createRoomSectionFooterTypeRestricted: String {
+ return VectorL10n.tr("Vector", "create_room_section_footer_type_restricted")
+ }
+ /// ADDRESS
public static var createRoomSectionHeaderAddress: String {
return VectorL10n.tr("Vector", "create_room_section_header_address")
}
- /// Room encryption
+ /// ENCRYPTION
public static var createRoomSectionHeaderEncryption: String {
return VectorL10n.tr("Vector", "create_room_section_header_encryption")
}
- /// Room name
+ /// NAME
public static var createRoomSectionHeaderName: String {
return VectorL10n.tr("Vector", "create_room_section_header_name")
}
- /// Room topic (optional)
+ /// TOPIC (OPTIONAL)
public static var createRoomSectionHeaderTopic: String {
return VectorL10n.tr("Vector", "create_room_section_header_topic")
}
- /// Room type
+ /// WHO CAN ACCESS
public static var createRoomSectionHeaderType: String {
return VectorL10n.tr("Vector", "create_room_section_header_type")
}
- /// Show the room in the directory
+ /// Show in room directory
public static var createRoomShowInDirectory: String {
return VectorL10n.tr("Vector", "create_room_show_in_directory")
}
+ /// This will help people find and join.
+ public static var createRoomShowInDirectoryFooter: String {
+ return VectorL10n.tr("Vector", "create_room_show_in_directory_footer")
+ }
+ /// Suggest to space members
+ public static var createRoomSuggestRoom: String {
+ return VectorL10n.tr("Vector", "create_room_suggest_room")
+ }
+ /// Suggested rooms are promoted to space members as good to join.
+ public static var createRoomSuggestRoomFooter: String {
+ return VectorL10n.tr("Vector", "create_room_suggest_room_footer")
+ }
/// New Room
public static var createRoomTitle: String {
return VectorL10n.tr("Vector", "create_room_title")
}
- /// Private Room
+ /// Private Room (invite only)
public static var createRoomTypePrivate: String {
return VectorL10n.tr("Vector", "create_room_type_private")
}
- /// Public Room
+ /// Public Room (anyone)
public static var createRoomTypePublic: String {
return VectorL10n.tr("Vector", "create_room_type_public")
}
+ /// Space members
+ public static var createRoomTypeRestricted: String {
+ return VectorL10n.tr("Vector", "create_room_type_restricted")
+ }
/// Verify your other devices easier
public static var crossSigningSetupBannerSubtitle: String {
return VectorL10n.tr("Vector", "cross_signing_setup_banner_subtitle")
@@ -1619,6 +1651,10 @@ public class VectorL10n: NSObject {
public static var e2eRoomKeyRequestTitle: String {
return VectorL10n.tr("Vector", "e2e_room_key_request_title")
}
+ /// Edit
+ public static var edit: String {
+ return VectorL10n.tr("Vector", "edit")
+ }
/// Activities
public static var emojiPickerActivityCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_activity_category")
@@ -1827,6 +1863,10 @@ public class VectorL10n: NSObject {
public static func eventFormatterWidgetRemovedByYou(_ p1: String) -> String {
return VectorL10n.tr("Vector", "event_formatter_widget_removed_by_you", p1)
}
+ /// Existing
+ public static var existing: String {
+ return VectorL10n.tr("Vector", "existing")
+ }
/// The link %@ is taking you to another site: %@\n\nAre you sure you want to continue?
public static func externalLinkConfirmationMessage(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "external_link_confirmation_message", p1, p2)
@@ -2667,6 +2707,18 @@ public class VectorL10n: NSObject {
public static var less: String {
return VectorL10n.tr("Vector", "less")
}
+ /// Stop
+ public static var liveLocationSharingBannerStop: String {
+ return VectorL10n.tr("Vector", "live_location_sharing_banner_stop")
+ }
+ /// Live location enabled
+ public static var liveLocationSharingBannerTitle: String {
+ return VectorL10n.tr("Vector", "live_location_sharing_banner_title")
+ }
+ /// Loading
+ public static var loading: String {
+ return VectorL10n.tr("Vector", "loading")
+ }
/// To discover contacts already using Matrix, %@ can send email addresses and phone numbers in your address book to your chosen Matrix identity server. Where supported, personal data is hashed before sending - please check your identity server's privacy policy for more details.
public static func localContactsAccessDiscoveryWarning(_ p1: String) -> String {
return VectorL10n.tr("Vector", "local_contacts_access_discovery_warning", p1)
@@ -3047,6 +3099,10 @@ public class VectorL10n: NSObject {
public static var networkOfflinePrompt: String {
return VectorL10n.tr("Vector", "network_offline_prompt")
}
+ /// New
+ public static var newWord: String {
+ return VectorL10n.tr("Vector", "new_word")
+ }
/// Next
public static var next: String {
return VectorL10n.tr("Vector", "next")
@@ -4099,6 +4155,74 @@ public class VectorL10n: NSObject {
public static var retry: String {
return VectorL10n.tr("Vector", "retry")
}
+ /// Edit spaces
+ public static var roomAccessSettingsScreenEditSpaces: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_edit_spaces")
+ }
+ /// Decide who can find and join %@.
+ public static func roomAccessSettingsScreenMessage(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_message", p1)
+ }
+ /// Room access
+ public static var roomAccessSettingsScreenNavTitle: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_nav_title")
+ }
+ /// Only invited people can find and join.
+ public static var roomAccessSettingsScreenPrivateMessage: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_private_message")
+ }
+ /// Anyone can find and join.
+ public static var roomAccessSettingsScreenPublicMessage: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_public_message")
+ }
+ /// Let anyone in a space find and join.\nYou’ll be asked to confirm which spaces.
+ public static var roomAccessSettingsScreenRestrictedMessage: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_restricted_message")
+ }
+ /// Setting room access
+ public static var roomAccessSettingsScreenSettingRoomAccess: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_setting_room_access")
+ }
+ /// Who can access this room?
+ public static var roomAccessSettingsScreenTitle: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_title")
+ }
+ /// Automatically invite members to new room
+ public static var roomAccessSettingsScreenUpgradeAlertAutoInviteSwitch: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_auto_invite_switch")
+ }
+ /// Anyone in Space name will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.\n\nPlease note upgrading will make a new version of the room. All current messages will stay in this archived room.
+ public static var roomAccessSettingsScreenUpgradeAlertMessage: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_message")
+ }
+ /// Upgrade room
+ public static var roomAccessSettingsScreenUpgradeAlertTitle: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_title")
+ }
+ /// Upgrade
+ public static var roomAccessSettingsScreenUpgradeAlertUpgradeButton: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_upgrade_button")
+ }
+ /// Upgrading room
+ public static var roomAccessSettingsScreenUpgradeAlertUpgrading: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_upgrading")
+ }
+ /// Upgrade required
+ public static var roomAccessSettingsScreenUpgradeRequired: String {
+ return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_required")
+ }
+ /// Spaces you know containing %@
+ public static func roomAccessSpaceChooserKnownSpacesSection(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "room_access_space_chooser_known_spaces_section", p1)
+ }
+ /// Other spaces or rooms
+ public static var roomAccessSpaceChooserOtherSpacesSection: String {
+ return VectorL10n.tr("Vector", "room_access_space_chooser_other_spaces_section")
+ }
+ /// These are likely things other admins of %@ are a part of.
+ public static func roomAccessSpaceChooserOtherSpacesSectionInfo(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "room_access_space_chooser_other_spaces_section_info", p1)
+ }
/// Call
public static var roomAccessibilityCall: String {
return VectorL10n.tr("Vector", "room_accessibility_call")
@@ -4267,6 +4391,10 @@ public class VectorL10n: NSObject {
public static var roomDeleteUnsentMessages: String {
return VectorL10n.tr("Vector", "room_delete_unsent_messages")
}
+ /// Access
+ public static var roomDetailsAccessRowTitle: String {
+ return VectorL10n.tr("Vector", "room_details_access_row_title")
+ }
/// Who can access this room?
public static var roomDetailsAccessSection: String {
return VectorL10n.tr("Vector", "room_details_access_section")
@@ -4531,6 +4659,14 @@ public class VectorL10n: NSObject {
public static var roomDetailsPhotoForDm: String {
return VectorL10n.tr("Vector", "room_details_photo_for_dm")
}
+ /// Suggest to space members
+ public static var roomDetailsPromoteRoomSuggestTitle: String {
+ return VectorL10n.tr("Vector", "room_details_promote_room_suggest_title")
+ }
+ /// Promote room
+ public static var roomDetailsPromoteRoomTitle: String {
+ return VectorL10n.tr("Vector", "room_details_promote_room_title")
+ }
/// Room Name
public static var roomDetailsRoomName: String {
return VectorL10n.tr("Vector", "room_details_room_name")
@@ -4915,6 +5051,26 @@ public class VectorL10n: NSObject {
public static var roomIntroCellInformationRoomWithoutTopicSentence2Part2: String {
return VectorL10n.tr("Vector", "room_intro_cell_information_room_without_topic_sentence2_part2")
}
+ /// You do not have permission to invite people to this room
+ public static var roomInviteNotEnoughPermission: String {
+ return VectorL10n.tr("Vector", "room_invite_not_enough_permission")
+ }
+ /// They won’t be a part of %@.
+ public static func roomInviteToRoomOptionDetail(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "room_invite_to_room_option_detail", p1)
+ }
+ /// To just this room
+ public static var roomInviteToRoomOptionTitle: String {
+ return VectorL10n.tr("Vector", "room_invite_to_room_option_title")
+ }
+ /// They can explore %@, but won’t be a member of %@.
+ public static func roomInviteToSpaceOptionDetail(_ p1: String, _ p2: String) -> String {
+ return VectorL10n.tr("Vector", "room_invite_to_space_option_detail", p1, p2)
+ }
+ /// To %@
+ public static func roomInviteToSpaceOptionTitle(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "room_invite_to_space_option_title", p1)
+ }
/// Join
public static var roomJoinGroupCall: String {
return VectorL10n.tr("Vector", "room_join_group_call")
@@ -5219,6 +5375,10 @@ public class VectorL10n: NSObject {
public static var roomParticipantsInvitePromptTitle: String {
return VectorL10n.tr("Vector", "room_participants_invite_prompt_title")
}
+ /// Are you sure you want to invite %@ to %@?
+ public static func roomParticipantsInvitePromptToMsg(_ p1: String, _ p2: String) -> String {
+ return VectorL10n.tr("Vector", "room_participants_invite_prompt_to_msg", p1, p2)
+ }
/// INVITED
public static var roomParticipantsInvitedSection: String {
return VectorL10n.tr("Vector", "room_participants_invited_section")
@@ -5463,6 +5623,18 @@ public class VectorL10n: NSObject {
public static var roomSlideToEndGroupCall: String {
return VectorL10n.tr("Vector", "room_slide_to_end_group_call")
}
+ /// Suggested rooms are promoted to space members as good ones to join.
+ public static var roomSuggestionSettingsScreenMessage: String {
+ return VectorL10n.tr("Vector", "room_suggestion_settings_screen_message")
+ }
+ /// Suggest room
+ public static var roomSuggestionSettingsScreenNavTitle: String {
+ return VectorL10n.tr("Vector", "room_suggestion_settings_screen_nav_title")
+ }
+ /// Make a room suggested in a space
+ public static var roomSuggestionSettingsScreenTitle: String {
+ return VectorL10n.tr("Vector", "room_suggestion_settings_screen_title")
+ }
/// Thread
public static var roomThreadTitle: String {
return VectorL10n.tr("Vector", "room_thread_title")
@@ -5567,6 +5739,10 @@ public class VectorL10n: NSObject {
public static var save: String {
return VectorL10n.tr("Vector", "save")
}
+ /// Saving
+ public static var saving: String {
+ return VectorL10n.tr("Vector", "saving")
+ }
/// Search
public static var searchDefaultPlaceholder: String {
return VectorL10n.tr("Vector", "search_default_placeholder")
@@ -6811,6 +6987,18 @@ public class VectorL10n: NSObject {
public static var shareExtensionSendNow: String {
return VectorL10n.tr("Vector", "share_extension_send_now")
}
+ /// Share invite link
+ public static var shareInviteLinkAction: String {
+ return VectorL10n.tr("Vector", "share_invite_link_action")
+ }
+ /// Hey, join this room on %@
+ public static func shareInviteLinkRoomText(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "share_invite_link_room_text", p1)
+ }
+ /// Hey, join this space on %@
+ public static func shareInviteLinkSpaceText(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "share_invite_link_space_text", p1)
+ }
/// Show Details
public static var showDetails: String {
return VectorL10n.tr("Vector", "show_details")
@@ -6835,6 +7023,10 @@ public class VectorL10n: NSObject {
public static func sideMenuAppVersion(_ p1: String) -> String {
return VectorL10n.tr("Vector", "side_menu_app_version", p1)
}
+ /// Swipe right or tap to see all rooms
+ public static var sideMenuCoachMessage: String {
+ return VectorL10n.tr("Vector", "side_menu_coach_message")
+ }
/// Left panel
public static var sideMenuRevealActionAccessibilityLabel: String {
return VectorL10n.tr("Vector", "side_menu_reveal_action_accessibility_label")
@@ -6959,6 +7151,10 @@ public class VectorL10n: NSObject {
public static var spaceHomeShowAllRooms: String {
return VectorL10n.tr("Vector", "space_home_show_all_rooms")
}
+ /// You do not have permission to invite people to this space
+ public static var spaceInviteNotEnoughPermission: String {
+ return VectorL10n.tr("Vector", "space_invite_not_enough_permission")
+ }
/// Ban from this space
public static var spaceParticipantsActionBan: String {
return VectorL10n.tr("Vector", "space_participants_action_ban")
@@ -6971,18 +7167,58 @@ public class VectorL10n: NSObject {
public static var spacePrivateJoinRule: String {
return VectorL10n.tr("Vector", "space_private_join_rule")
}
+ /// Invite only, best for yourself or teams
+ public static var spacePrivateJoinRuleDetail: String {
+ return VectorL10n.tr("Vector", "space_private_join_rule_detail")
+ }
/// Public space
public static var spacePublicJoinRule: String {
return VectorL10n.tr("Vector", "space_public_join_rule")
}
+ /// Open to anyone, best for communities
+ public static var spacePublicJoinRuleDetail: String {
+ return VectorL10n.tr("Vector", "space_public_join_rule_detail")
+ }
+ /// Who can access this space?
+ public static var spaceSettingsAccessSection: String {
+ return VectorL10n.tr("Vector", "space_settings_access_section")
+ }
+ /// Your space is viewable at\n%@
+ public static func spaceSettingsCurrentAddressMessage(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "space_settings_current_address_message", p1)
+ }
+ /// Failed to update space settings. Do you want to retry?
+ public static var spaceSettingsUpdateFailedMessage: String {
+ return VectorL10n.tr("Vector", "space_settings_update_failed_message")
+ }
/// space
public static var spaceTag: String {
return VectorL10n.tr("Vector", "space_tag")
}
+ /// Description
+ public static var spaceTopic: String {
+ return VectorL10n.tr("Vector", "space_topic")
+ }
+ /// Add room
+ public static var spacesAddRoom: String {
+ return VectorL10n.tr("Vector", "spaces_add_room")
+ }
+ /// You do not have permissions to add rooms to this space.
+ public static var spacesAddRoomMissingPermissionMessage: String {
+ return VectorL10n.tr("Vector", "spaces_add_room_missing_permission_message")
+ }
/// Adding rooms coming soon
public static var spacesAddRoomsComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title")
}
+ /// Add space
+ public static var spacesAddSpace: String {
+ return VectorL10n.tr("Vector", "spaces_add_space")
+ }
+ /// Create space
+ public static var spacesAddSpaceTitle: String {
+ return VectorL10n.tr("Vector", "spaces_add_space_title")
+ }
/// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with %@ on your computer.
public static func spacesComingSoonDetail(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_coming_soon_detail", p1)
@@ -6991,6 +7227,190 @@ public class VectorL10n: NSObject {
public static var spacesComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_coming_soon_title")
}
+ /// Create a space
+ public static var spacesCreateSpaceTitle: String {
+ return VectorL10n.tr("Vector", "spaces_create_space_title")
+ }
+ /// As this space is just for you, no one will be informed. You can add more later.
+ public static var spacesCreationAddRoomsMessage: String {
+ return VectorL10n.tr("Vector", "spaces_creation_add_rooms_message")
+ }
+ /// What do you want to add?
+ public static var spacesCreationAddRoomsTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_add_rooms_title")
+ }
+ /// Address
+ public static var spacesCreationAddress: String {
+ return VectorL10n.tr("Vector", "spaces_creation_address")
+ }
+ /// %@\nalready exists
+ public static func spacesCreationAddressAlreadyExists(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_address_already_exists", p1)
+ }
+ /// Your space will be viewable at\n%@
+ public static func spacesCreationAddressDefaultMessage(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_address_default_message", p1)
+ }
+ /// %@\nhas invalid characters
+ public static func spacesCreationAddressInvalidCharacters(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_address_invalid_characters", p1)
+ }
+ /// Your progress will be lost.
+ public static var spacesCreationCancelMessage: String {
+ return VectorL10n.tr("Vector", "spaces_creation_cancel_message")
+ }
+ /// Stop creating a space?
+ public static var spacesCreationCancelTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_cancel_title")
+ }
+ /// Email
+ public static var spacesCreationEmailInvitesEmailTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_email_invites_email_title")
+ }
+ /// You can invite them later too.
+ public static var spacesCreationEmailInvitesMessage: String {
+ return VectorL10n.tr("Vector", "spaces_creation_email_invites_message")
+ }
+ /// Invite your team
+ public static var spacesCreationEmailInvitesTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_email_invites_title")
+ }
+ /// Name required
+ public static var spacesCreationEmptyRoomNameError: String {
+ return VectorL10n.tr("Vector", "spaces_creation_empty_room_name_error")
+ }
+ /// You can change this later
+ public static var spacesCreationFooter: String {
+ return VectorL10n.tr("Vector", "spaces_creation_footer")
+ }
+ /// Spaces are a new way to group rooms and people.
+ public static var spacesCreationHint: String {
+ return VectorL10n.tr("Vector", "spaces_creation_hint")
+ }
+ /// in %@ spaces
+ public static func spacesCreationInManySpaces(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_in_many_spaces", p1)
+ }
+ /// in 1 space
+ public static var spacesCreationInOneSpace: String {
+ return VectorL10n.tr("Vector", "spaces_creation_in_one_space")
+ }
+ /// in %@
+ public static func spacesCreationInSpacename(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_in_spacename", p1)
+ }
+ /// in %@ + %@ spaces
+ public static func spacesCreationInSpacenamePlusMany(_ p1: String, _ p2: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_in_spacename_plus_many", p1, p2)
+ }
+ /// in %@ + 1 space
+ public static func spacesCreationInSpacenamePlusOne(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_in_spacename_plus_one", p1)
+ }
+ /// Invite by username
+ public static var spacesCreationInviteByUsername: String {
+ return VectorL10n.tr("Vector", "spaces_creation_invite_by_username")
+ }
+ /// You can invite them later too.
+ public static var spacesCreationInviteByUsernameMessage: String {
+ return VectorL10n.tr("Vector", "spaces_creation_invite_by_username_message")
+ }
+ /// Invite your team
+ public static var spacesCreationInviteByUsernameTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_invite_by_username_title")
+ }
+ /// General
+ public static var spacesCreationNewRoomsGeneral: String {
+ return VectorL10n.tr("Vector", "spaces_creation_new_rooms_general")
+ }
+ /// We’ll create a room for each one.
+ public static var spacesCreationNewRoomsMessage: String {
+ return VectorL10n.tr("Vector", "spaces_creation_new_rooms_message")
+ }
+ /// Random
+ public static var spacesCreationNewRoomsRandom: String {
+ return VectorL10n.tr("Vector", "spaces_creation_new_rooms_random")
+ }
+ /// Room name
+ public static var spacesCreationNewRoomsRoomNameTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_new_rooms_room_name_title")
+ }
+ /// Support
+ public static var spacesCreationNewRoomsSupport: String {
+ return VectorL10n.tr("Vector", "spaces_creation_new_rooms_support")
+ }
+ /// What are some discussions you’ll have?
+ public static var spacesCreationNewRoomsTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_new_rooms_title")
+ }
+ /// Adding %@ rooms
+ public static func spacesCreationPostProcessAddingRooms(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_post_process_adding_rooms", p1)
+ }
+ /// Creating %@
+ public static func spacesCreationPostProcessCreatingRoom(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_post_process_creating_room", p1)
+ }
+ /// Creating space
+ public static var spacesCreationPostProcessCreatingSpace: String {
+ return VectorL10n.tr("Vector", "spaces_creation_post_process_creating_space")
+ }
+ /// Creating %@
+ public static func spacesCreationPostProcessCreatingSpaceTask(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_post_process_creating_space_task", p1)
+ }
+ /// Inviting %@ users
+ public static func spacesCreationPostProcessInvitingUsers(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_post_process_inviting_users", p1)
+ }
+ /// Uploading avatar
+ public static var spacesCreationPostProcessUploadingAvatar: String {
+ return VectorL10n.tr("Vector", "spaces_creation_post_process_uploading_avatar")
+ }
+ /// Your private space
+ public static var spacesCreationPrivateSpaceTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_private_space_title")
+ }
+ /// Your public space
+ public static var spacesCreationPublicSpaceTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_public_space_title")
+ }
+ /// Add some details to help it stand out. You can change these at any point.
+ public static var spacesCreationSettingsMessage: String {
+ return VectorL10n.tr("Vector", "spaces_creation_settings_message")
+ }
+ /// A private space to organise your rooms
+ public static var spacesCreationSharingTypeJustMeDetail: String {
+ return VectorL10n.tr("Vector", "spaces_creation_sharing_type_just_me_detail")
+ }
+ /// Just me
+ public static var spacesCreationSharingTypeJustMeTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_sharing_type_just_me_title")
+ }
+ /// A private space for you & your teammates
+ public static var spacesCreationSharingTypeMeAndTeammatesDetail: String {
+ return VectorL10n.tr("Vector", "spaces_creation_sharing_type_me_and_teammates_detail")
+ }
+ /// Me and teammates
+ public static var spacesCreationSharingTypeMeAndTeammatesTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_sharing_type_me_and_teammates_title")
+ }
+ /// Make sure the right people have access %@. You can change this later.
+ public static func spacesCreationSharingTypeMessage(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_creation_sharing_type_message", p1)
+ }
+ /// Who are you working with?
+ public static var spacesCreationSharingTypeTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_sharing_type_title")
+ }
+ /// To join an existing space, you need an invite.
+ public static var spacesCreationVisibilityMessage: String {
+ return VectorL10n.tr("Vector", "spaces_creation_visibility_message")
+ }
+ /// What type of space do you want to create?
+ public static var spacesCreationVisibilityTitle: String {
+ return VectorL10n.tr("Vector", "spaces_creation_visibility_title")
+ }
/// Some rooms may be hidden because they’re private and you need an invite.
public static var spacesEmptySpaceDetail: String {
return VectorL10n.tr("Vector", "spaces_empty_space_detail")
@@ -7003,10 +7423,22 @@ public class VectorL10n: NSObject {
public static var spacesExploreRooms: String {
return VectorL10n.tr("Vector", "spaces_explore_rooms")
}
+ /// 1 room
+ public static var spacesExploreRoomsOneRoom: String {
+ return VectorL10n.tr("Vector", "spaces_explore_rooms_one_room")
+ }
+ /// %@ rooms
+ public static func spacesExploreRoomsRoomNumber(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "spaces_explore_rooms_room_number", p1)
+ }
/// Home
public static var spacesHomeSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_home_space_title")
}
+ /// Invite people
+ public static var spacesInvitePeople: String {
+ return VectorL10n.tr("Vector", "spaces_invite_people")
+ }
/// Invites coming soon
public static var spacesInvitesComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_invites_coming_soon_title")
@@ -7091,6 +7523,10 @@ public class VectorL10n: NSObject {
public static var startVoiceCall: String {
return VectorL10n.tr("Vector", "start_voice_call")
}
+ /// Stop
+ public static var stop: String {
+ return VectorL10n.tr("Vector", "stop")
+ }
/// Element is a new type of messenger and collaboration app that:\n\n1. Puts you in control to preserve your privacy\n2. Lets you communicate with anyone in the Matrix network, and even beyond by integrating with apps such as Slack\n3. Protects you from advertising, datamining, backdoors and walled gardens\n4. Secures you through end-to-end encryption, with cross-signing to verify others\n\nElement is completely different from other messaging and collaboration apps because it is decentralised and open source.\n\nElement lets you self-host - or choose a host - so that you have privacy, ownership and control of your data and conversations. It gives you access to an open network; so you’re not just stuck speaking to other Element users only. And it is very secure.\n\nElement is able to do all this because it operates on Matrix - the standard for open, decentralised communication. \n\nElement puts you in control by letting you choose who hosts your conversations. From the Element app, you can choose to host in different ways:\n\n1. Get a free account on the matrix.org public server\n2. Self-host your account by running a server on your own hardware\n3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform\n\nWhy choose Element?\n\nOWN YOUR DATA: You decide where to keep your data and messages. You own it and control it, not some MEGACORP that mines your data or gives access to third parties.\n\nOPEN MESSAGING AND COLLABORATION: You can chat with anyone else in the Matrix network, whether they’re using Element or another Matrix app, and even if they are using a different messaging system of the likes of Slack, IRC or XMPP.\n\nSUPER-SECURE: Real end-to-end encryption (only those in the conversation can decrypt messages), and cross-signing to verify the devices of conversation participants.\n\nCOMPLETE COMMUNICATION: Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done.\n\nEVERYWHERE YOU ARE: Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://element.io/app.
public static var storeFullDescription: String {
return VectorL10n.tr("Vector", "store_full_description")
@@ -7111,6 +7547,10 @@ public class VectorL10n: NSObject {
public static var submitCode: String {
return VectorL10n.tr("Vector", "submit_code")
}
+ /// Suggest
+ public static var suggest: String {
+ return VectorL10n.tr("Vector", "suggest")
+ }
/// Switch
public static var `switch`: String {
return VectorL10n.tr("Vector", "switch")
diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift
index efc032506..8be6e7646 100644
--- a/Riot/Generated/UntranslatedStrings.swift
+++ b/Riot/Generated/UntranslatedStrings.swift
@@ -10,22 +10,66 @@ import Foundation
// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
public extension VectorL10n {
+ /// Choose from files
+ static var imagePickerActionFiles: String {
+ return VectorL10n.tr("Untranslated", "image_picker_action_files")
+ }
+ /// Profile picture
+ static var onboardingAvatarAccessibilityLabel: String {
+ return VectorL10n.tr("Untranslated", "onboarding_avatar_accessibility_label")
+ }
+ /// You can change this anytime.
+ static var onboardingAvatarMessage: String {
+ return VectorL10n.tr("Untranslated", "onboarding_avatar_message")
+ }
+ /// Add a profile picture
+ static var onboardingAvatarTitle: String {
+ return VectorL10n.tr("Untranslated", "onboarding_avatar_title")
+ }
/// Take me home
static var onboardingCongratulationsHomeButton: String {
return VectorL10n.tr("Untranslated", "onboarding_congratulations_home_button")
}
- /// Your account\n%@\nhas been created.
+ /// Your account %@ has been created.
public static func onboardingCongratulationsMessage(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "onboarding_congratulations_message", p1)
}
/// Personalise profile
- static var onboardingCongratulationsPersonaliseButton: String {
- return VectorL10n.tr("Untranslated", "onboarding_congratulations_personalise_button")
+ static var onboardingCongratulationsPersonalizeButton: String {
+ return VectorL10n.tr("Untranslated", "onboarding_congratulations_personalize_button")
}
/// Congratulations!
static var onboardingCongratulationsTitle: String {
return VectorL10n.tr("Untranslated", "onboarding_congratulations_title")
}
+ /// You can change this later
+ static var onboardingDisplayNameHint: String {
+ return VectorL10n.tr("Untranslated", "onboarding_display_name_hint")
+ }
+ /// Your display name must be less than 256 characters
+ static var onboardingDisplayNameMaxLength: String {
+ return VectorL10n.tr("Untranslated", "onboarding_display_name_max_length")
+ }
+ /// This will be shown when you send messages.
+ static var onboardingDisplayNameMessage: String {
+ return VectorL10n.tr("Untranslated", "onboarding_display_name_message")
+ }
+ /// Display Name
+ static var onboardingDisplayNamePlaceholder: String {
+ return VectorL10n.tr("Untranslated", "onboarding_display_name_placeholder")
+ }
+ /// Choose a display name
+ static var onboardingDisplayNameTitle: String {
+ return VectorL10n.tr("Untranslated", "onboarding_display_name_title")
+ }
+ /// Save and continue
+ static var onboardingPersonalizationSave: String {
+ return VectorL10n.tr("Untranslated", "onboarding_personalization_save")
+ }
+ /// Skip this step
+ static var onboardingPersonalizationSkip: String {
+ return VectorL10n.tr("Untranslated", "onboarding_personalization_skip")
+ }
}
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
diff --git a/Riot/Managers/PushNotification/PushNotificationService.h b/Riot/Managers/PushNotification/PushNotificationService.h
index c16b65792..5f0e368c9 100644
--- a/Riot/Managers/PushNotification/PushNotificationService.h
+++ b/Riot/Managers/PushNotification/PushNotificationService.h
@@ -110,7 +110,8 @@ NS_ASSUME_NONNULL_BEGIN
@param roomId Room identifier to be navigated.
*/
- (void)pushNotificationService:(PushNotificationService *)pushNotificationService
- shouldNavigateToRoomWithId:(NSString *)roomId;
+ shouldNavigateToRoomWithId:(NSString *)roomId
+ threadId:(nullable NSString *)threadId;
@end;
diff --git a/Riot/Managers/PushNotification/PushNotificationService.m b/Riot/Managers/PushNotification/PushNotificationService.m
index 59fb9604a..e48a3acfa 100644
--- a/Riot/Managers/PushNotification/PushNotificationService.m
+++ b/Riot/Managers/PushNotification/PushNotificationService.m
@@ -363,6 +363,7 @@ Matrix session observer used to detect new opened sessions.
UNNotificationContent *content = notification.request.content;
NSString *actionIdentifier = [response actionIdentifier];
NSString *roomId = content.userInfo[@"room_id"];
+ NSString *threadId = content.userInfo[@"thread_id"];
if ([actionIdentifier isEqualToString:@"inline-reply"])
{
@@ -371,7 +372,10 @@ Matrix session observer used to detect new opened sessions.
UNTextInputNotificationResponse *textInputNotificationResponse = (UNTextInputNotificationResponse *)response;
NSString *responseText = [textInputNotificationResponse userText];
- [self handleNotificationInlineReplyForRoomId:roomId withResponseText:responseText success:^(NSString *eventId) {
+ [self handleNotificationInlineReplyForRoomId:roomId
+ threadId:threadId
+ withResponseText:responseText
+ success:^(NSString *eventId) {
completionHandler();
} failure:^(NSError *error) {
@@ -399,7 +403,7 @@ Matrix session observer used to detect new opened sessions.
}
else if ([actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier])
{
- [self notifyNavigateToRoomById:roomId];
+ [self notifyNavigateToRoomById:roomId threadId:threadId];
completionHandler();
}
else
@@ -412,6 +416,7 @@ Matrix session observer used to detect new opened sessions.
#pragma mark - Other Methods
- (void)handleNotificationInlineReplyForRoomId:(NSString*)roomId
+ threadId:(NSString*)threadId
withResponseText:(NSString*)responseText
success:(void(^)(NSString *eventId))success
failure:(void(^)(NSError *error))failure
@@ -424,35 +429,35 @@ Matrix session observer used to detect new opened sessions.
NSArray* mxAccounts = [MXKAccountManager sharedManager].activeAccounts;
- __block MXKRoomDataSourceManager* manager;
- dispatch_group_t group = dispatch_group_create();
+ __block MXSession *mxSession;
+ dispatch_group_t dispatchGroupSession = dispatch_group_create();
for (MXKAccount* account in mxAccounts)
{
void(^storeDataReadyBlock)(void) = ^{
- MXRoom* room = [account.mxSession roomWithRoomId:roomId];
+ MXRoom *room = [account.mxSession roomWithRoomId:roomId];
if (room)
{
- manager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:account.mxSession];
+ mxSession = account.mxSession;
}
};
if (account.mxSession.state >= MXSessionStateStoreDataReady)
{
storeDataReadyBlock();
- if (manager)
+ if (mxSession)
{
break;
}
}
else
{
- dispatch_group_enter(group);
+ dispatch_group_enter(dispatchGroupSession);
// wait for session state to be store data ready
id sessionStateObserver = nil;
sessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:account.mxSession queue:nil usingBlock:^(NSNotification * _Nonnull note) {
- if (manager)
+ if (mxSession)
{
[[NSNotificationCenter defaultCenter] removeObserver:sessionStateObserver];
return;
@@ -462,25 +467,51 @@ Matrix session observer used to detect new opened sessions.
{
[[NSNotificationCenter defaultCenter] removeObserver:sessionStateObserver];
storeDataReadyBlock();
- dispatch_group_leave(group);
+ dispatch_group_leave(dispatchGroupSession);
}
}];
}
}
- dispatch_group_notify(group, dispatch_get_main_queue(), ^{
- if (manager == nil)
+ dispatch_group_notify(dispatchGroupSession, dispatch_get_main_queue(), ^{
+ if (mxSession == nil)
{
MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: room with id %@ not found", roomId);
failure(nil);
}
else
{
- [manager roomDataSourceForRoom:roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) {
+ // initialize data source for a thread or a room
+ __block MXKRoomDataSource *dataSource;
+ dispatch_group_t dispatchGroupDataSource = dispatch_group_create();
+ if (RiotSettings.shared.enableThreads && threadId)
+ {
+ dispatch_group_enter(dispatchGroupDataSource);
+ [ThreadDataSource loadRoomDataSourceWithRoomId:roomId
+ initialEventId:nil
+ threadId:threadId
+ andMatrixSession:mxSession
+ onComplete:^(MXKRoomDataSource *threadDataSource) {
+ dataSource = threadDataSource;
+ dispatch_group_leave(dispatchGroupDataSource);
+ }];
+ }
+ else
+ {
+ dispatch_group_enter(dispatchGroupDataSource);
+ MXKRoomDataSourceManager *manager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession];
+ [manager roomDataSourceForRoom:roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) {
+ dataSource = roomDataSource;
+ dispatch_group_leave(dispatchGroupDataSource);
+ }];
+ }
+
+ dispatch_group_notify(dispatchGroupDataSource, dispatch_get_main_queue(), ^{
if (responseText != nil && responseText.length != 0)
{
- MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: sending message to room: %@", roomId);
- [roomDataSource sendTextMessage:responseText success:^(NSString* eventId) {
+ NSString *logForThread = threadId ? [NSString stringWithFormat:@", thread: %@", threadId] : nil;
+ MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: sending message to room: %@%@", roomId, logForThread);
+ [dataSource sendTextMessage:responseText success:^(NSString* eventId) {
success(eventId);
} failure:^(NSError* error) {
failure(error);
@@ -490,7 +521,7 @@ Matrix session observer used to detect new opened sessions.
{
failure(nil);
}
- }];
+ });
}
});
}
@@ -536,11 +567,11 @@ Matrix session observer used to detect new opened sessions.
#pragma mark - Delegate Notifiers
-- (void)notifyNavigateToRoomById:(NSString *)roomId
+- (void)notifyNavigateToRoomById:(NSString *)roomId threadId:(NSString *)threadId
{
- if ([_delegate respondsToSelector:@selector(pushNotificationService:shouldNavigateToRoomWithId:)])
+ if ([_delegate respondsToSelector:@selector(pushNotificationService:shouldNavigateToRoomWithId:threadId:)])
{
- [_delegate pushNotificationService:self shouldNavigateToRoomWithId:roomId];
+ [_delegate pushNotificationService:self shouldNavigateToRoomWithId:roomId threadId:threadId];
}
}
diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift
index e034f28f5..71f44b1d8 100644
--- a/Riot/Managers/Settings/RiotSettings.swift
+++ b/Riot/Managers/Settings/RiotSettings.swift
@@ -352,4 +352,13 @@ final class RiotSettings: NSObject {
@UserDefault(key: "versionCheckNextDisplayDateTimeInterval", defaultValue: 0.0, storage: defaults)
var versionCheckNextDisplayDateTimeInterval
+
+ @UserDefault(key: "slideMenuRoomsCoachMessageHasBeenDisplayed", defaultValue: false, storage: defaults)
+ var slideMenuRoomsCoachMessageHasBeenDisplayed
+
+ // MARK: - Metrics
+
+ /// Number of spaces previously tracked by the `AnalyticsSpaceTracker` instance.
+ @UserDefault(key: "lastNumberOfTrackedSpaces", defaultValue: nil, storage: defaults)
+ var lastNumberOfTrackedSpaces: Int?
}
diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift
index f8e0a382d..93d9cb2a9 100644
--- a/Riot/Managers/Theme/Theme.swift
+++ b/Riot/Managers/Theme/Theme.swift
@@ -112,7 +112,7 @@ import DesignKit
/// - Parameter tabBar: The tab bar to customise.
func applyStyle(onTabBar tabBar: UITabBar)
- /// Apply the theme on a navigation bar, without enabling the iOS 15's scroll edges appearance.
+ /// Apply the theme on a navigation bar, without enabling the iOS 15's scroll edge appearance.
///
/// - Parameter navigationBar: the navigation bar to customise.
func applyStyle(onNavigationBar navigationBar: UINavigationBar)
@@ -120,9 +120,9 @@ import DesignKit
/// Apply the theme on a navigation bar.
///
/// - Parameter navigationBar: the navigation bar to customise.
- /// - Parameter modernScrollEdgesAppearance: whether or not to use the iOS 15 style scroll edges appearance
+ /// - Parameter modernScrollEdgeAppearance: whether or not to use the iOS 15 style scroll edge appearance
func applyStyle(onNavigationBar navigationBar: UINavigationBar,
- withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool)
+ withModernScrollEdgeAppearance modernScrollEdgeAppearance: Bool)
/// Apply the theme on a search bar.
///
diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift
index ae19db7e7..d61ed3954 100644
--- a/Riot/Managers/Theme/Themes/DarkTheme.swift
+++ b/Riot/Managers/Theme/Themes/DarkTheme.swift
@@ -114,11 +114,11 @@ class DarkTheme: NSObject, Theme {
// Protocols don't support default parameter values and a protocol extension won't work for @objc
func applyStyle(onNavigationBar navigationBar: UINavigationBar) {
- applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: false)
+ applyStyle(onNavigationBar: navigationBar, withModernScrollEdgeAppearance: false)
}
func applyStyle(onNavigationBar navigationBar: UINavigationBar,
- withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool) {
+ withModernScrollEdgeAppearance modernScrollEdgeAppearance: Bool) {
navigationBar.tintColor = tintColor
// On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style.
@@ -127,7 +127,7 @@ class DarkTheme: NSObject, Theme {
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = baseColor
- if !modernScrollEdgesAppearance {
+ if !modernScrollEdgeAppearance {
appearance.shadowColor = nil
}
appearance.titleTextAttributes = [
@@ -135,7 +135,7 @@ class DarkTheme: NSObject, Theme {
]
navigationBar.standardAppearance = appearance
- navigationBar.scrollEdgeAppearance = modernScrollEdgesAppearance ? nil : appearance
+ navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance
} else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift
index a7832ba07..e2afd9339 100644
--- a/Riot/Managers/Theme/Themes/DefaultTheme.swift
+++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift
@@ -120,11 +120,11 @@ class DefaultTheme: NSObject, Theme {
// Protocols don't support default parameter values and a protocol extension doesn't work for @objc
func applyStyle(onNavigationBar navigationBar: UINavigationBar) {
- applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: false)
+ applyStyle(onNavigationBar: navigationBar, withModernScrollEdgeAppearance: false)
}
func applyStyle(onNavigationBar navigationBar: UINavigationBar,
- withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool) {
+ withModernScrollEdgeAppearance modernScrollEdgeAppearance: Bool) {
navigationBar.tintColor = tintColor
// On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style.
@@ -133,7 +133,7 @@ class DefaultTheme: NSObject, Theme {
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = baseColor
- if !modernScrollEdgesAppearance {
+ if !modernScrollEdgeAppearance {
appearance.shadowColor = nil
}
appearance.titleTextAttributes = [
@@ -141,7 +141,7 @@ class DefaultTheme: NSObject, Theme {
]
navigationBar.standardAppearance = appearance
- navigationBar.scrollEdgeAppearance = modernScrollEdgesAppearance ? nil : appearance
+ navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance
} else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift
index 581133ce8..994e4fb2c 100644
--- a/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift
+++ b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift
@@ -22,14 +22,14 @@ final class HomeserverConfiguration: NSObject {
// Note: Use an object per configuration subject when there is multiple properties related
let jitsi: HomeserverJitsiConfiguration
- let isE2EEByDefaultEnabled: Bool
+ let encryption: HomeserverEncryptionConfiguration
let tileServer: HomeserverTileServerConfiguration
init(jitsi: HomeserverJitsiConfiguration,
- isE2EEByDefaultEnabled: Bool,
+ encryption: HomeserverEncryptionConfiguration,
tileServer: HomeserverTileServerConfiguration) {
self.jitsi = jitsi
- self.isE2EEByDefaultEnabled = isE2EEByDefaultEnabled
+ self.encryption = encryption
self.tileServer = tileServer
}
}
diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift
index ca87788b6..5b9224806 100644
--- a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift
+++ b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift
@@ -28,10 +28,6 @@ final class HomeserverConfigurationBuilder: NSObject {
/// Create an `HomeserverConfiguration` from an HS Well-Known when possible otherwise it takes hardcoded values from BuildSettings by default.
func build(from wellKnown: MXWellKnown?) -> HomeserverConfiguration {
-
- let isE2EEByDefaultEnabled: Bool
- let jitsiPreferredDomain: String
-
var vectorWellKnownEncryptionConfiguration: VectorWellKnownEncryptionConfiguration?
var vectorWellKnownJitsiConfiguration: VectorWellKnownJitsiConfiguration?
@@ -39,12 +35,26 @@ final class HomeserverConfigurationBuilder: NSObject {
vectorWellKnownEncryptionConfiguration = self.getEncryptionConfiguration(from: vectorWellKnown)
vectorWellKnownJitsiConfiguration = self.getJitsiConfiguration(from: vectorWellKnown)
}
-
+
// Encryption configuration
// Enable E2EE by default when there is no value
- isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfiguration?.isE2EEByDefaultEnabled ?? true
+ let isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfiguration?.isE2EEByDefaultEnabled ?? true
+ // Disable mandatory secure backup when there is no value
+ let isSecureBackupRequired = vectorWellKnownEncryptionConfiguration?.isSecureBackupRequired ?? false
+ // Defaults to all secure backup methods available when there is no value
+ let secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod]
+ if let backupSetupMethods = vectorWellKnownEncryptionConfiguration?.secureBackupSetupMethods {
+ secureBackupSetupMethods = backupSetupMethods.isEmpty ? VectorWellKnownBackupSetupMethod.allCases : backupSetupMethods
+ } else {
+ secureBackupSetupMethods = VectorWellKnownBackupSetupMethod.allCases
+ }
+
+ let encryptionConfiguration = HomeserverEncryptionConfiguration(isE2EEByDefaultEnabled: isE2EEByDefaultEnabled,
+ isSecureBackupRequired: isSecureBackupRequired,
+ secureBackupSetupMethods: secureBackupSetupMethods)
// Jitsi configuration
+ let jitsiPreferredDomain: String
let jitsiServerURL: URL
let hardcodedJitsiServerURL: URL = BuildSettings.jitsiServerUrl
@@ -77,7 +87,7 @@ final class HomeserverConfigurationBuilder: NSObject {
serverURL: jitsiServerURL)
return HomeserverConfiguration(jitsi: jitsiConfiguration,
- isE2EEByDefaultEnabled: isE2EEByDefaultEnabled,
+ encryption: encryptionConfiguration,
tileServer: tileServerConfiguration)
}
diff --git a/Riot/Model/HomeserverConfiguration/HomeserverEncryptionConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverEncryptionConfiguration.swift
new file mode 100644
index 000000000..19b9aaee1
--- /dev/null
+++ b/Riot/Model/HomeserverConfiguration/HomeserverEncryptionConfiguration.swift
@@ -0,0 +1,35 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+
+/// `HomeserverEncryptionConfiguration` gives encryption configuration used by homeserver
+@objcMembers
+final class HomeserverEncryptionConfiguration: NSObject {
+ let isE2EEByDefaultEnabled: Bool
+ let isSecureBackupRequired: Bool
+ let secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod]
+
+ init(isE2EEByDefaultEnabled: Bool,
+ isSecureBackupRequired: Bool,
+ secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod]) {
+ self.isE2EEByDefaultEnabled = isE2EEByDefaultEnabled
+ self.isSecureBackupRequired = isSecureBackupRequired
+ self.secureBackupSetupMethods = secureBackupSetupMethods
+
+ super.init()
+ }
+}
diff --git a/Riot/Model/WellKnown/VectorWellKnown.swift b/Riot/Model/WellKnown/VectorWellKnown.swift
index dd548d6b2..8d5669b21 100644
--- a/Riot/Model/WellKnown/VectorWellKnown.swift
+++ b/Riot/Model/WellKnown/VectorWellKnown.swift
@@ -41,14 +41,29 @@ extension VectorWellKnown: Decodable {
}
// MARK: - Encryption
-
-struct VectorWellKnownEncryptionConfiguration: Decodable {
-
+struct VectorWellKnownEncryptionConfiguration {
/// Indicate if E2EE is enabled by default
let isE2EEByDefaultEnabled: Bool?
-
+ /// Check if secure backup (SSSS) is mandatory.
+ let isSecureBackupRequired: Bool?
+ /// Methods to use to setup secure backup (SSSS).
+ let secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod]?
+}
+
+extension VectorWellKnownEncryptionConfiguration: Decodable {
+ /// JSON keys associated to `VectorWellKnownEncryptionConfiguration`
enum CodingKeys: String, CodingKey {
case isE2EEByDefaultEnabled = "default"
+ case isSecureBackupRequired = "secure_backup_required"
+ case secureBackupSetupMethods = "secure_backup_setup_methods"
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ isE2EEByDefaultEnabled = try? container.decode(Bool.self, forKey: .isE2EEByDefaultEnabled)
+ isSecureBackupRequired = try? container.decode(Bool.self, forKey: .isSecureBackupRequired)
+ let secureBackupSetupMethodsKeys = try? container.decode([String].self, forKey: .secureBackupSetupMethods)
+ secureBackupSetupMethods = secureBackupSetupMethodsKeys?.compactMap { VectorWellKnownBackupSetupMethod(key: $0) }
}
}
diff --git a/Riot/Model/WellKnown/VectorWellKnownBackupSetupMethod.swift b/Riot/Model/WellKnown/VectorWellKnownBackupSetupMethod.swift
new file mode 100644
index 000000000..a5a241c91
--- /dev/null
+++ b/Riot/Model/WellKnown/VectorWellKnownBackupSetupMethod.swift
@@ -0,0 +1,39 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+
+/// Methods to use to setup secure backup (SSSS).
+@objc enum VectorWellKnownBackupSetupMethod: Int, CaseIterable {
+ case passphrase = 0
+ case key
+
+ private enum Constants {
+ static let setupMethodPassphrase: String = "passphrase"
+ static let setupMethodKey: String = "key"
+ }
+
+ init?(key: String) {
+ switch key {
+ case Constants.setupMethodPassphrase:
+ self = .passphrase
+ case Constants.setupMethodKey:
+ self = .key
+ default:
+ return nil
+ }
+ }
+}
diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift
index c6296b28d..74880e69f 100644
--- a/Riot/Modules/Analytics/Analytics.swift
+++ b/Riot/Modules/Analytics/Analytics.swift
@@ -45,6 +45,8 @@ import AnalyticsEvents
/// The service used to interact with account data settings.
private var service: AnalyticsService?
+ private var viewRoomActiveSpace: AnalyticsViewRoomActiveSpace = .home
+
/// Whether or not the object is enabled and sending events to the server.
var isRunning: Bool { client.isRunning }
@@ -59,6 +61,31 @@ import AnalyticsEvents
RiotSettings.shared.hasAcceptedMatomoAnalytics
}
+ /// Used to defined the trigger of the next potential `JoinedRoom` event
+ var joinedRoomTrigger: AnalyticsJoinedRoomTrigger = .unknown
+
+ /// Used to defined the trigger of the next potential `ViewRoom` event
+ var viewRoomTrigger: AnalyticsViewRoomTrigger = .unknown
+
+ /// Used to defined the actual space activated by the user.
+ var activeSpace: MXSpace? {
+ didSet {
+ updateViewRoomActiveSpace()
+ }
+ }
+
+ /// Used to defined the currently visible space in explore rooms.
+ var exploringSpace: MXSpace? {
+ didSet {
+ updateViewRoomActiveSpace()
+ }
+ }
+
+ // MARK: - Private
+
+ /// keep an instance of `AnalyticsSpaceTracker` to track space metrics when space graph is built.
+ private let spaceTracker: AnalyticsSpaceTracker = AnalyticsSpaceTracker()
+
// MARK: - Public
/// Opts in to analytics tracking with the supplied session.
@@ -94,8 +121,13 @@ import AnalyticsEvents
MXLog.debug("[Analytics] Started.")
- // Catch and log crashes
- MXLogger.logCrashes(true)
+ if Bundle.main.isShareExtension {
+ // Don't log crashes in the share extension
+ } else {
+ // Catch and log crashes
+ MXLogger.logCrashes(true)
+ }
+
MXLogger.setBuildVersion(AppInfo.current.buildInfo.readableBuildVersion)
}
@@ -165,6 +197,19 @@ import AnalyticsEvents
private func capture(event: AnalyticsEventProtocol) {
client.capture(event)
}
+
+ /// Update `viewRoomActiveSpace` property according to the current value of `exploringSpace` and `activeSpace` properties.
+ private func updateViewRoomActiveSpace() {
+ let space = exploringSpace ?? activeSpace
+ guard let spaceRoom = space?.room else {
+ viewRoomActiveSpace = .home
+ return
+ }
+
+ spaceRoom.state { roomState in
+ self.viewRoomActiveSpace = roomState?.isJoinRulePublic == true ? .public : .private
+ }
+ }
}
// MARK: - Public tracking methods
@@ -174,10 +219,10 @@ extension Analytics {
/// Updates any user properties to help with creating cohorts.
///
/// Only non-nil properties will be updated when calling this method.
- func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil) {
+ func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil, numFavouriteRooms: Int? = nil, numSpaces: Int? = nil) {
let userProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: ftueUseCase?.analyticsName,
- numFavouriteRooms: nil,
- numSpaces: nil)
+ numFavouriteRooms: numFavouriteRooms,
+ numSpaces: numSpaces)
client.updateUserProperties(userProperties)
}
@@ -242,6 +287,27 @@ extension Analytics {
func trackIdentityServerAccepted(_ accepted: Bool) {
// Do we still want to track this?
}
+
+ /// Track view room event triggered when the user changes rooms.
+ /// - Parameters:
+ /// - room: the room being viewed
+ func trackViewRoom(_ room: MXRoom) {
+ trackViewRoom(asDM: room.isDirect, isSpace: room.summary?.roomType == .space)
+ }
+
+ /// Track view room event triggered when the user changes rooms.
+ /// - Parameters:
+ /// - isDM: Whether the room is a DM.
+ /// - isSpace: Whether the room is a Space.
+ func trackViewRoom(asDM isDM: Bool, isSpace: Bool) {
+ let event = AnalyticsEvent.ViewRoom(activeSpace: viewRoomActiveSpace.space,
+ isDM: isDM,
+ isSpace: isSpace,
+ trigger: viewRoomTrigger.trigger,
+ viaKeyboard: nil)
+ viewRoomTrigger = .unknown
+ capture(event: event)
+ }
}
// MARK: - MXAnalyticsDelegate
@@ -284,8 +350,10 @@ extension Analytics: MXAnalyticsDelegate {
return
}
- let event = AnalyticsEvent.JoinedRoom(isDM: isDM, isSpace: isSpace, roomSize: roomSize, trigger: nil)
+ let event = AnalyticsEvent.JoinedRoom(isDM: isDM, isSpace: isSpace, roomSize: roomSize, trigger: joinedRoomTrigger.trigger)
capture(event: event)
+
+ self.joinedRoomTrigger = .unknown
}
/// **Note** This method isn't currently implemented.
diff --git a/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift b/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift
new file mode 100644
index 000000000..2b830870c
--- /dev/null
+++ b/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift
@@ -0,0 +1,50 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+import AnalyticsEvents
+
+@objc enum AnalyticsJoinedRoomTrigger: Int {
+ case unknown
+ case invite
+ case notification
+ case roomDirectory
+ case roomPreview
+ case slashCommand
+ case spaceHierarchy
+ case timeline
+
+ var trigger: AnalyticsEvent.JoinedRoom.Trigger? {
+ switch self {
+ case .unknown:
+ return nil
+ case .invite:
+ return .Invite
+ case .notification:
+ return .Notification
+ case .roomDirectory:
+ return .RoomDirectory
+ case .roomPreview:
+ return .RoomPreview
+ case .slashCommand:
+ return .SlashCommand
+ case .spaceHierarchy:
+ return .SpaceHierarchy
+ case .timeline:
+ return .Timeline
+ }
+ }
+}
diff --git a/Riot/Modules/Analytics/AnalyticsSpaceTracker.swift b/Riot/Modules/Analytics/AnalyticsSpaceTracker.swift
new file mode 100644
index 000000000..85aad36b7
--- /dev/null
+++ b/Riot/Modules/Analytics/AnalyticsSpaceTracker.swift
@@ -0,0 +1,47 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+
+class AnalyticsSpaceTracker {
+
+ // MARK: - Setup
+
+ init() {
+ NotificationCenter.default.addObserver(self, selector: #selector(self.spaceGraphDidUpdate(notification:)), name: MXSpaceService.didBuildSpaceGraph, object: nil)
+ }
+
+ @objc private func spaceGraphDidUpdate(notification: Notification) {
+ guard let spaceService = notification.object as? MXSpaceService else {
+ return
+ }
+
+ trackSpaceNumber(with: spaceService)
+ }
+
+ // MARK: - Private
+
+ private func trackSpaceNumber(with spaceService: MXSpaceService) {
+ let spaceNumber = spaceService.spaceSummaries.filter { $0.membership == .join }.count
+
+ guard RiotSettings.shared.lastNumberOfTrackedSpaces != spaceNumber else {
+ return
+ }
+
+ Analytics.shared.updateUserProperties(numSpaces: spaceNumber)
+ RiotSettings.shared.lastNumberOfTrackedSpaces = spaceNumber
+ }
+}
diff --git a/Riot/Modules/Analytics/AnalyticsUIElement.swift b/Riot/Modules/Analytics/AnalyticsUIElement.swift
index bd44a7458..75a976126 100644
--- a/Riot/Modules/Analytics/AnalyticsUIElement.swift
+++ b/Riot/Modules/Analytics/AnalyticsUIElement.swift
@@ -22,6 +22,8 @@ import AnalyticsEvents
case roomThreadSummaryItem
case threadListThreadItem
case threadListFilterItem
+ case spacePanelSelectedSpace
+ case spacePanelSwitchSpace
/// The element name reported to the AnalyticsEvent.
var name: AnalyticsEvent.Interaction.Name {
@@ -34,6 +36,10 @@ import AnalyticsEvents
return .MobileThreadListThreadItem
case .threadListFilterItem:
return .MobileThreadListFilterItem
+ case .spacePanelSelectedSpace:
+ return .SpacePanelSelectedSpace
+ case .spacePanelSwitchSpace:
+ return .SpacePanelSwitchSpace
}
}
}
diff --git a/Riot/Modules/Analytics/AnalyticsViewRoomActiveSpace.swift b/Riot/Modules/Analytics/AnalyticsViewRoomActiveSpace.swift
new file mode 100644
index 000000000..20f541a15
--- /dev/null
+++ b/Riot/Modules/Analytics/AnalyticsViewRoomActiveSpace.swift
@@ -0,0 +1,41 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+import AnalyticsEvents
+
+@objc enum AnalyticsViewRoomActiveSpace: Int {
+ case unknown
+ case home
+ case meta
+ case `private`
+ case `public`
+
+ var space: AnalyticsEvent.ViewRoom.ActiveSpace? {
+ switch self {
+ case .unknown:
+ return nil
+ case .home:
+ return .Home
+ case .meta:
+ return .Meta
+ case .private:
+ return .Private
+ case .public:
+ return .Public
+ }
+ }
+}
diff --git a/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift
new file mode 100644
index 000000000..1be27326a
--- /dev/null
+++ b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift
@@ -0,0 +1,89 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+import AnalyticsEvents
+
+@objc enum AnalyticsViewRoomTrigger: Int {
+ case unknown
+ case created
+ case messageSearch
+ case messageUser
+ case notification
+ case predecessor
+ case roomDirectory
+ case roomList
+ case spaceHierarchy
+ case timeline
+ case tombstone
+ case verificationRequest
+ case widget
+ case roomMemberDetail
+ case fileSearch
+ case roomSearch
+ case searchContactDetail
+ case spaceMemberDetail
+ case inCall
+ case spaceMenu
+ case spaceSettings
+
+ var trigger: AnalyticsEvent.ViewRoom.Trigger? {
+ switch self {
+ case .unknown:
+ return nil
+ case .created:
+ return .Created
+ case .messageSearch:
+ return .MessageSearch
+ case .messageUser:
+ return .MessageUser
+ case .notification:
+ return .Notification
+ case .predecessor:
+ return .Predecessor
+ case .roomDirectory:
+ return .RoomDirectory
+ case .roomList:
+ return .RoomList
+ case .spaceHierarchy:
+ return .SpaceHierarchy
+ case .timeline:
+ return .Timeline
+ case .tombstone:
+ return .Tombstone
+ case .verificationRequest:
+ return .VerificationRequest
+ case .widget:
+ return .Widget
+ case .fileSearch:
+ return .MobileFileSearch
+ case .roomSearch:
+ return .MobileRoomSearch
+ case .roomMemberDetail:
+ return .MobileRoomMemberDetail
+ case .searchContactDetail:
+ return .MobileSearchContactDetail
+ case .spaceMemberDetail:
+ return .MobileSpaceMemberDetail
+ case .inCall:
+ return .MobileInCall
+ case .spaceMenu:
+ return .MobileSpaceMenu
+ case .spaceSettings:
+ return .MobileSpaceSettings
+ }
+ }
+}
diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift
index 3413a90ad..e218a1a11 100755
--- a/Riot/Modules/Application/AppCoordinator.swift
+++ b/Riot/Modules/Application/AppCoordinator.swift
@@ -213,9 +213,11 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
case .homeSpace:
MXLog.verbose("Switch to home space")
self.navigateToSpace(with: nil)
+ Analytics.shared.activeSpace = nil
case .space(let spaceId):
MXLog.verbose("Switch to space with id: \(spaceId)")
self.navigateToSpace(with: spaceId)
+ Analytics.shared.activeSpace = userSessionsService.mainUserSession?.matrixSession.spaceService.getSpace(withId: spaceId)
}
}
@@ -249,7 +251,8 @@ extension AppCoordinator: LegacyAppDelegateDelegate {
func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didAddMatrixSession session: MXSession!) {
}
- func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didRemoveMatrixSession session: MXSession!) {
+ func legacyAppDelegate(_ legacyAppDelegate: LegacyAppDelegate!, didRemoveMatrixSession session: MXSession?) {
+ guard let session = session else { return }
// Handle user session removal on clear cache. On clear cache the account has his session closed but the account is not removed.
self.userSessionsService.removeUserSession(relatedToMatrixSession: session)
}
diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m
index cb03f978f..989ed7b95 100644
--- a/Riot/Modules/Application/LegacyAppDelegate.m
+++ b/Riot/Modules/Application/LegacyAppDelegate.m
@@ -85,7 +85,7 @@ NSString *const AppDelegateDidValidateEmailNotificationClientSecretKey = @"AppDe
NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUniversalLinkDidChangeNotification";
-@interface LegacyAppDelegate ()
+@interface LegacyAppDelegate ()
{
/**
Reachability observer
@@ -129,6 +129,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
*/
KeyVerificationCoordinatorBridgePresenter *keyVerificationCoordinatorBridgePresenter;
+ /**
+ Currently displayed secure backup setup
+ */
+ SecureBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter;
+
/**
Account picker used in case of multiple account.
*/
@@ -1100,10 +1105,25 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
#pragma mark - PushNotificationServiceDelegate
-- (void)pushNotificationService:(PushNotificationService *)pushNotificationService shouldNavigateToRoomWithId:(NSString *)roomId
+- (void)pushNotificationService:(PushNotificationService *)pushNotificationService
+ shouldNavigateToRoomWithId:(NSString *)roomId
+ threadId:(NSString *)threadId
{
+ if (roomId)
+ {
+ MXRoom *room = [self.mxSessions.firstObject roomWithRoomId:roomId];
+ if (room.summary.membership != MXMembershipJoin)
+ {
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerNotification;
+ }
+ else
+ {
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerNotification;
+ }
+ }
+
_lastNavigatedRoomIdFromPush = roomId;
- [self navigateToRoomById:roomId];
+ [self navigateToRoomById:roomId threadId:threadId];
}
#pragma mark - Badge Count
@@ -2033,7 +2053,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountManagerDidSoftlogoutAccountNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXKAccount *account = notif.object;
- [self removeMatrixSession:account.mxSession];
+
+ if (account.mxSession)
+ {
+ [self removeMatrixSession:account.mxSession];
+ }
// Return to authentication screen
[self.masterTabBarController showSoftLogoutOnboardingFlowWithCredentials:account.mxCredentials];
@@ -2389,6 +2413,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
isLaunching = YES;
break;
case MXSessionStateStoreDataReady:
+ case MXSessionStateProcessingBackgroundSyncCache:
case MXSessionStateSyncInProgress:
// Stay in launching during the first server sync if the store is empty.
isLaunching = (mainSession.rooms.count == 0 && launchAnimationContainerView);
@@ -2428,6 +2453,15 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// wait for another session state change to check room list data is ready
return;
}
+
+ if (mainSession.vc_homeserverConfiguration.encryption.isSecureBackupRequired
+ && mainSession.vc_canSetupSecureBackup)
+ {
+ // This only happens at the first login
+ // Or when migrating an existing user
+ MXLogDebug(@"[AppDelegate] handleAppState: Force SSSS setup");
+ [self presentSecureBackupSetupForSession:mainSession];
+ }
void (^finishAppLaunch)(void) = ^{
[self hideLaunchAnimation];
@@ -2878,7 +2912,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
#pragma mark - Matrix Rooms handling
-- (void)navigateToRoomById:(NSString *)roomId
+- (void)navigateToRoomById:(NSString *)roomId threadId:(NSString *)threadId
{
if (roomId.length)
{
@@ -2912,7 +2946,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
{
MXLogDebug(@"[AppDelegate][Push] navigateToRoomById: open the roomViewController %@", roomId);
- [self showRoom:roomId andEventId:nil withMatrixSession:dedicatedAccount.mxSession];
+ [self showRoom:roomId
+ threadId:threadId
+ andEventId:nil
+ withMatrixSession:dedicatedAccount.mxSession];
}
else
{
@@ -2936,6 +2973,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
{
MXRoom *room = [mxSession roomWithRoomId:roomId];
+ if (room && room.summary.membership == MXMembershipJoin)
+ {
+ [Analytics.shared trackViewRoom:room];
+ }
+
// Indicates that spaces are not supported
if (room.summary.roomType == MXRoomTypeSpace)
{
@@ -2978,14 +3020,25 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
}
- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession
+{
+ [self showRoom:roomId threadId:nil andEventId:eventId withMatrixSession:mxSession];
+}
+
+- (void)showRoom:(NSString*)roomId threadId:(NSString*)threadId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession
{
// Ask to restore initial display
ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES];
-
+
+ ThreadParameters *threadParameters = nil;
+ if (RiotSettings.shared.enableThreads && threadId)
+ {
+ threadParameters = [[ThreadParameters alloc] initWithThreadId:threadId stackRoomScreen:NO];
+ }
+
RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId
eventId:eventId
mxSession:mxSession
- threadParameters:nil
+ threadParameters:threadParameters
presentationParameters:presentationParameters];
[self showRoomWithParameters:parameters];
@@ -3156,6 +3209,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[mxSession createRoomWithParameters:roomCreationParameters success:^(MXRoom *room) {
// Open created room
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated;
[self showRoom:room.roomId andEventId:nil withMatrixSession:mxSession];
if (completion)
@@ -3190,6 +3244,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
if (directRoom)
{
// open it
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated;
[self showRoom:directRoom.roomId andEventId:nil withMatrixSession:mxSession];
if (completion)
@@ -4276,6 +4331,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
if (!keyVerificationCoordinatorBridgePresenter.isPresenting)
{
keyVerificationCoordinatorBridgePresenter = [[KeyVerificationCoordinatorBridgePresenter alloc] initWithSession:mxSession];
+ keyVerificationCoordinatorBridgePresenter.cancellable = !mxSession.vc_homeserverConfiguration.encryption.isSecureBackupRequired;
keyVerificationCoordinatorBridgePresenter.delegate = self;
[keyVerificationCoordinatorBridgePresenter presentCompleteSecurityFrom:self.presentedViewController isNewSignIn:NO animated:YES];
@@ -4656,4 +4712,37 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[self openSpaceWithId:spaceId];
}
+#pragma mark - Mandatory SSSS setup
+
+- (void)presentSecureBackupSetupForSession:(MXSession*)mxSession
+{
+ MXLogDebug(@"[AppDelegate][Mandatory SSSS] presentSecureBackupSetupForSession");
+
+ if (!secureBackupSetupCoordinatorBridgePresenter.isPresenting)
+ {
+ secureBackupSetupCoordinatorBridgePresenter = [[SecureBackupSetupCoordinatorBridgePresenter alloc] initWithSession:mxSession allowOverwrite:false];
+ secureBackupSetupCoordinatorBridgePresenter.delegate = self;
+
+ [secureBackupSetupCoordinatorBridgePresenter presentFrom:self.masterTabBarController animated:NO cancellable:NO];
+ }
+ else
+ {
+ MXLogDebug(@"[AppDelegate][Mandatory SSSS] presentSecureBackupSetupForSession: Controller already presented")
+ }
+}
+
+#pragma mark - SecureBackupSetupCoordinatorBridgePresenterDelegate
+
+- (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ [secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
+ secureBackupSetupCoordinatorBridgePresenter = nil;
+}
+
+- (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidCancel:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ [secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
+ secureBackupSetupCoordinatorBridgePresenter = nil;
+}
+
@end
diff --git a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift
index b44d00356..7477cc7e4 100644
--- a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift
+++ b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift
@@ -55,6 +55,9 @@ class RoomNavigationParameters: NSObject {
/// Screen presentation parameters.
let presentationParameters: ScreenPresentationParameters
+ /// If `true`, the room settings screen will be initially displayed. Default `false`
+ let showSettingsInitially: Bool
+
// MARK: - Setup
init(roomId: String,
@@ -67,6 +70,22 @@ class RoomNavigationParameters: NSObject {
self.mxSession = mxSession
self.threadParameters = threadParameters
self.presentationParameters = presentationParameters
+ self.showSettingsInitially = false
+
+ super.init()
+ }
+
+ init(roomId: String,
+ eventId: String?,
+ mxSession: MXSession,
+ presentationParameters: ScreenPresentationParameters,
+ showSettingsInitially: Bool) {
+ self.roomId = roomId
+ self.eventId = eventId
+ self.mxSession = mxSession
+ self.presentationParameters = presentationParameters
+ self.showSettingsInitially = showSettingsInitially
+ self.threadParameters = nil
super.init()
}
diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
index e452d8529..3bcae3348 100644
--- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
@@ -127,7 +127,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
}
let isNewSignIn = true
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: session, flow: .completeSecurity(isNewSignIn))
+ let cancellable = !session.vc_homeserverConfiguration().encryption.isSecureBackupRequired
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: session, flow: .completeSecurity(isNewSignIn), cancellable: cancellable)
keyVerificationCoordinator.delegate = self
let presentable = keyVerificationCoordinator.toPresentable()
@@ -176,7 +177,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
// TODO: This is still not sure we want to disable the automatic cross-signing bootstrap
// if the admin disabled e2e by default.
// Do like riot-web for the moment
- if session.vc_homeserverConfiguration().isE2EEByDefaultEnabled {
+ if session.vc_homeserverConfiguration().encryption.isE2EEByDefaultEnabled {
// Bootstrap cross-signing on user's account
// We do it for both registration and new login as long as cross-signing does not exist yet
if let password = self.password, !password.isEmpty {
diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m
index 448d75b8b..c8507dc5e 100644
--- a/Riot/Modules/Authentication/AuthenticationViewController.m
+++ b/Riot/Modules/Authentication/AuthenticationViewController.m
@@ -211,7 +211,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
- (void)userInterfaceThemeDidChange
{
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar
- withModernScrollEdgesAppearance:YES];
+ withModernScrollEdgeAppearance:YES];
self.view.backgroundColor = ThemeService.shared.theme.backgroundColor;
diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m
index 8703e18c7..71c52ba72 100644
--- a/Riot/Modules/Call/CallViewController.m
+++ b/Riot/Modules/Call/CallViewController.m
@@ -563,6 +563,7 @@ CallAudioRouteMenuViewDelegate>
if (self.mxCall.room)
{
// Open the room page
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerInCall;
[[AppDelegate theDelegate] showRoom:self.mxCall.room.roomId andEventId:nil withMatrixSession:self.mxCall.room.mxSession];
}
diff --git a/Riot/Modules/Camera/CameraPresenter.swift b/Riot/Modules/Camera/CameraPresenter.swift
index 12373bee9..863115d23 100644
--- a/Riot/Modules/Camera/CameraPresenter.swift
+++ b/Riot/Modules/Camera/CameraPresenter.swift
@@ -19,7 +19,7 @@ import UIKit
import AVFoundation
@objc protocol CameraPresenterDelegate: AnyObject {
- func cameraPresenter(_ presenter: CameraPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
+ func cameraPresenter(_ presenter: CameraPresenter, didSelectImage image: UIImage)
func cameraPresenter(_ presenter: CameraPresenter, didSelectVideoAt url: URL)
func cameraPresenterDidCancel(_ cameraPresenter: CameraPresenter)
}
@@ -27,12 +27,6 @@ import AVFoundation
/// CameraPresenter enables to present native camera
@objc final class CameraPresenter: NSObject {
- // MARK: - Constants
-
- private enum Constants {
- static let jpegCompressionQuality: CGFloat = 1.0
- }
-
// MARK: - Properties
// MARK: Private
@@ -131,8 +125,8 @@ extension CameraPresenter: UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let videoURL = info[.mediaURL] as? URL {
self.delegate?.cameraPresenter(self, didSelectVideoAt: videoURL)
- } else if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage, let imageData = image.jpegData(compressionQuality: Constants.jpegCompressionQuality) {
- self.delegate?.cameraPresenter(self, didSelectImageData: imageData, withUTI: MXKUTI.jpeg)
+ } else if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage {
+ self.delegate?.cameraPresenter(self, didSelectImage: image)
}
}
diff --git a/Riot/Modules/Common/CoachMessages/CoachMarkView.swift b/Riot/Modules/Common/CoachMessages/CoachMarkView.swift
new file mode 100644
index 000000000..3c88f8e57
--- /dev/null
+++ b/Riot/Modules/Common/CoachMessages/CoachMarkView.swift
@@ -0,0 +1,128 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 UIKit
+import Reusable
+
+/// `CoachMarkView` is used to display an information bubble view with a given text.
+@objcMembers
+class CoachMarkView: UIView, NibLoadable, Themable {
+
+ // MARK: Constants
+
+ public static var TopLeftPosition: CGPoint {
+ if UIDevice.current.orientation.isPortrait {
+ return CGPoint(x: 16, y: 40)
+ } else {
+ return CGPoint(x: 16, y: 32)
+ }
+ }
+
+ enum MarkPosition: Int {
+ case topLeft
+ case topRight
+ case bottomLeft
+ case bottomRight
+ }
+
+ // MARK: Private
+
+ @IBOutlet private weak var backgroundView: UIImageView!
+ @IBOutlet private weak var textLabel: UILabel!
+ @IBOutlet private weak var textLabelTopMargin: NSLayoutConstraint!
+ @IBOutlet private weak var textLabelBottomMargin: NSLayoutConstraint!
+
+ private var text: String? {
+ didSet {
+ textLabel.text = text
+ }
+ }
+ private var markPosition: MarkPosition = .topLeft
+ private var position: CGPoint!
+
+ // MARK: Setup
+
+ class func instantiate(text: String?, from position: CGPoint, markPosition: MarkPosition) -> Self {
+ let view = Self.loadFromNib()
+ view.text = text
+ view.position = position
+ view.markPosition = markPosition
+ return view
+ }
+
+ // MARK: UIView
+
+ override func willMove(toSuperview newSuperview: UIView?) {
+ super.willMove(toSuperview: newSuperview)
+
+ setupView()
+ if newSuperview != nil {
+ update(theme: ThemeService.shared().theme)
+ }
+ }
+
+ override func didMoveToSuperview() {
+ super.didMoveToSuperview()
+
+ guard let superview = superview else {
+ return
+ }
+
+ let layoutGuide = superview.safeAreaLayoutGuide
+ translatesAutoresizingMaskIntoConstraints = false
+ leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor, constant: position.x).isActive = true
+ topAnchor.constraint(equalTo: layoutGuide.topAnchor, constant: position.y).isActive = true
+ }
+
+ // MARK: Themable
+
+ func update(theme: Theme) {
+ textLabel.textColor = theme.colors.background
+ textLabel.font = theme.fonts.bodySB
+ backgroundView.tintColor = theme.colors.accent
+ }
+
+ // MARK: Private
+
+ private func setupView() {
+ let image: UIImage = Asset.Images.coachMark.image
+ let imageSize = image.size
+ let center = CGPoint(x: ceil(imageSize.width / 2), y: ceil(imageSize.height / 2))
+
+ backgroundView.image = image.resizableImage(withCapInsets: .init(top: center.y - 1, left: center.x - 1, bottom: center.y + 1, right: center.x + 1), resizingMode: .stretch)
+
+ switch markPosition {
+ case .topLeft:
+ backgroundView.transform = .identity
+ case .topRight:
+ backgroundView.transform = .init(scaleX: -1, y: 1)
+ case .bottomLeft:
+ backgroundView.transform = .init(scaleX: 1, y: -1)
+ invertVerticalMargins()
+ case .bottomRight:
+ backgroundView.transform = .init(scaleX: -1, y: -1)
+ invertVerticalMargins()
+ }
+
+ textLabel.text = text
+ }
+
+ private func invertVerticalMargins() {
+ let temp = self.textLabelTopMargin.constant
+ self.textLabelTopMargin.constant = self.textLabelBottomMargin.constant
+ self.textLabelBottomMargin.constant = temp
+ }
+}
diff --git a/Riot/Modules/Common/CoachMessages/CoachMarkView.xib b/Riot/Modules/Common/CoachMessages/CoachMarkView.xib
new file mode 100644
index 000000000..6c37e9461
--- /dev/null
+++ b/Riot/Modules/Common/CoachMessages/CoachMarkView.xib
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/Common/CoachMessages/WindowOverlayPresenter.swift b/Riot/Modules/Common/CoachMessages/WindowOverlayPresenter.swift
new file mode 100644
index 000000000..97099cb63
--- /dev/null
+++ b/Riot/Modules/Common/CoachMessages/WindowOverlayPresenter.swift
@@ -0,0 +1,112 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 UIKit
+
+/// `WindowOverlayPresenter` allows to add a given view to the presenting view or window.
+/// The presenter also manages taps over the presenting view and the duration to dismiss the view.
+class WindowOverlayPresenter: NSObject {
+
+ // MARK: Private
+
+ private weak var presentingView: UIView?
+ private weak var presentedView: UIView?
+ private weak var gestureRecognizer: UIGestureRecognizer?
+ private var timer: Timer?
+
+ // MARK: Public
+
+ /// Add a given view to the presenting view or window.
+ /// The presenter also manages taps over the presenting view and the duration to dismiss the view.
+ ///
+ /// - parameters:
+ /// - view: instance of the view that will be displayed
+ /// - presentingView: instance of the presenting view. `nil` will display the view over the key window
+ /// - duration:if duration is not `nil`, the view will be dismissed after the given duration. The view is never dismissed otherwise
+ func show(_ view: UIView, over presentingView: UIView? = nil, duration: TimeInterval? = nil) {
+ guard presentedView == nil else {
+ return
+ }
+
+ let keyWindow: UIWindow? = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
+
+ guard let backView = presentingView ?? keyWindow else {
+ MXLog.error("[WindowOverlay] show: no eligible presenting view found")
+ return
+ }
+
+ view.alpha = 0
+
+ backView.addSubview(view)
+
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.didTapOnBackView(sender:)))
+ tapGestureRecognizer.cancelsTouchesInView = false
+ backView.addGestureRecognizer(tapGestureRecognizer)
+ self.gestureRecognizer = tapGestureRecognizer
+
+ self.presentingView = backView
+ self.presentedView = view
+
+ UIView.animate(withDuration: 0.3) {
+ view.alpha = 1
+ }
+
+ if let timeout = duration {
+ timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false, block: { [weak self] timer in
+ self?.dismiss()
+ })
+ }
+ }
+
+ /// Dismisses the currently presented view.
+ func dismiss() {
+ if let gestureRecognizer = self.gestureRecognizer {
+ self.presentingView?.removeGestureRecognizer(gestureRecognizer)
+ }
+ self.timer?.invalidate()
+ self.timer = nil
+
+ UIView.animate(withDuration: 0.3) {
+ self.presentedView?.alpha = 0
+ } completion: { isFinished in
+ if isFinished {
+ self.presentedView?.removeFromSuperview()
+ self.presentingView = nil
+ }
+ }
+
+ }
+
+ // MARK: Private
+
+ @objc private func didTapOnBackView(sender: UIGestureRecognizer) {
+ dismiss()
+ }
+}
+
+// MARK: Objective-C
+extension WindowOverlayPresenter {
+ /// Add a given view to the presenting view or window.
+ /// The presenter also manages taps over the presenting view and the duration to dismiss the view.
+ ///
+ /// - parameters:
+ /// - view: instance of the view that will be displayed
+ /// - presentingView: instance of the presenting view. `nil` will display the view over the key window
+ /// - duration:if duration > 0, the view will be dismissed after the given duration. The view is never dismissed otherwise
+ @objc func show(_ view: UIView, over presentingView: UIView?, duration: TimeInterval) {
+ self.show(view, over: presentingView, duration: duration > 0 ? duration : nil)
+ }
+}
diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
index 0430b3304..ec5a4916f 100644
--- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
+++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
@@ -195,6 +195,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
- (void)setCurrentSpace:(MXSpace *)currentSpace
{
+ super.currentSpace = currentSpace;
[self.recentsListService updateSpace:currentSpace];
}
diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h
index a9f46c8ff..6867e063d 100644
--- a/Riot/Modules/Common/Recents/RecentsViewController.h
+++ b/Riot/Modules/Common/Recents/RecentsViewController.h
@@ -147,6 +147,11 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification;
*/
- (void)onPlusButtonPressed;
+/**
+ Open screen to create a new chat room.
+ */
+- (void)startChat;
+
/**
Open screen to create a new room.
*/
diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m
index dab50e315..25aa17ec9 100644
--- a/Riot/Modules/Common/Recents/RecentsViewController.m
+++ b/Riot/Modules/Common/Recents/RecentsViewController.m
@@ -878,6 +878,12 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
- (void)showRoomWithRoomId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession
{
+ MXRoom *room = [matrixSession roomWithRoomId:roomId];
+ if (room.summary.membership == MXMembershipInvite)
+ {
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerInvite;
+ }
+
// Avoid multiple openings of rooms
self.userInteractionEnabled = NO;
@@ -897,6 +903,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
- (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData
{
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerRoomDirectory;
+
// Do not stack views when showing room
ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO sender:nil sourceView:nil];
@@ -993,6 +1001,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
}
// Accept invitation
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerInvite;
[self joinRoom:invitedRoom completion:nil];
}
else if ([actionIdentifier isEqualToString:kInviteRecentTableViewCellDeclineButtonPressed])
@@ -1892,7 +1901,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
typeof(self) self = weakSelf;
self->currentAlert = nil;
- [self performSegueWithIdentifier:@"presentStartChat" sender:self];
+ [self startChat];
}
}]];
@@ -2002,12 +2011,17 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
self.customSizedPresentationController = nil;
}
+- (void)startChat {
+ [self performSegueWithIdentifier:@"presentStartChat" sender:self];
+}
+
- (void)createNewRoom
{
// Sanity check
if (self.mainSession)
{
- self.createRoomCoordinatorBridgePresenter = [[CreateRoomCoordinatorBridgePresenter alloc] initWithSession:self.mainSession];
+ CreateRoomCoordinatorParameter *parameters = [[CreateRoomCoordinatorParameter alloc] initWithSession:self.mainSession parentSpace: self.dataSource.currentSpace];
+ self.createRoomCoordinatorBridgePresenter = [[CreateRoomCoordinatorBridgePresenter alloc] initWithParameters:parameters];
self.createRoomCoordinatorBridgePresenter.delegate = self;
[self.createRoomCoordinatorBridgePresenter presentFrom:self animated:YES];
}
@@ -2055,6 +2069,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
// Check whether the user has already joined the selected public room
if ([self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession isJoinedOnRoom:publicRoom.roomId])
{
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomDirectory;
+
// Open the public room
[self showRoomWithRoomId:publicRoom.roomId
inMatrixSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession];
@@ -2150,11 +2166,14 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString *)roomId inMatrixSession:(MXSession *)matrixSession
{
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomList;
[self showRoomWithRoomId:roomId inMatrixSession:matrixSession];
}
- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo
{
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerSpaceHierarchy;
+
RoomPreviewData *previewData = [[RoomPreviewData alloc] initWithSpaceChildInfo:childInfo andSession:self.mainSession];
[self startActivityIndicator];
MXWeakify(self);
@@ -2214,6 +2233,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
- (void)createRoomCoordinatorBridgePresenterDelegate:(CreateRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter didCreateNewRoom:(MXRoom *)room
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated;
[self showRoomWithRoomId:room.roomId inMatrixSession:self.mainSession];
}];
coordinatorBridgePresenter = nil;
@@ -2225,6 +2245,12 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
coordinatorBridgePresenter = nil;
}
+- (void)createRoomCoordinatorBridgePresenterDelegate:(CreateRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter didAddRoomsWithIds:(NSArray *)roomIds
+{
+ [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
+ coordinatorBridgePresenter = nil;
+}
+
#pragma mark - Empty view management
- (void)showEmptyViewIfNeeded
diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
index 8afaaa060..c79762b89 100644
--- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
+++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
@@ -26,11 +26,13 @@ class VectorHostingController: UIHostingController {
// MARK: Private
+ var isNavigationBarHidden: Bool = false
+ var hidesBackTitleWhenPushed: Bool = false
private var theme: Theme
// MARK: Public
- var enableNavigationBarScrollEdgesAppearance = false
+ var enableNavigationBarScrollEdgeAppearance = false
init(rootView: Content) where Content: View {
self.theme = ThemeService.shared().theme
@@ -52,6 +54,26 @@ class VectorHostingController: UIHostingController {
self.update(theme: self.theme)
}
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ if isNavigationBarHidden {
+ self.navigationController?.isNavigationBarHidden = true
+ }
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ if hidesBackTitleWhenPushed {
+ vc_removeBackTitle()
+ }
+
+ if navigationController?.isNavigationBarHidden ?? false {
+ navigationController?.interactivePopGestureRecognizer?.delegate = nil
+ }
+ }
+
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
@@ -71,7 +93,7 @@ class VectorHostingController: UIHostingController {
private func update(theme: Theme) {
if let navigationBar = self.navigationController?.navigationBar {
- theme.applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: enableNavigationBarScrollEdgesAppearance)
+ theme.applyStyle(onNavigationBar: navigationBar, withModernScrollEdgeAppearance: enableNavigationBarScrollEdgeAppearance)
}
}
}
diff --git a/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift b/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift
index 872a7191f..36c7c0c6e 100644
--- a/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift
+++ b/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift
@@ -47,26 +47,19 @@ protocol UserIndicatorTypePresenterProtocol {
}
class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol {
- private weak var viewController: UIViewController?
-
- // In the existing app architecture it is often view controllers which instantiate
- // various presenters (errors, alerts ... ) and present on self. Since the presenting view controller
- // needs to be passed on init, it must be declared as weak, otherwise a retain cycle would occur.
- private var presentingViewController: UIViewController {
- guard let viewController = viewController else {
- MXLog.error("[UserIndicatorTypePresenter]: Presenting view controller is not available")
- return UIViewController()
- }
- return viewController
- }
-
+ private let presentationContext: UserIndicatorPresentationContext
let queue: UserIndicatorQueue
- init(presentingViewController: UIViewController) {
- self.viewController = presentingViewController
+ init(presentationContext: UserIndicatorPresentationContext) {
+ self.presentationContext = presentationContext
self.queue = UserIndicatorQueue()
}
+ convenience init(presentingViewController: UIViewController) {
+ let context = StaticUserIndicatorPresentationContext(viewController: presentingViewController)
+ self.init(presentationContext: context)
+ }
+
func present(_ type: UserIndicatorType) -> UserIndicator {
let request = userIndicatorRequest(for: type)
return queue.add(request)
@@ -91,7 +84,7 @@ class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol {
style: .loading,
label: label
),
- presentingViewController: presentingViewController
+ presentationContext: presentationContext
)
return UserIndicatorRequest(
presenter: presenter,
@@ -102,7 +95,7 @@ class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol {
private func fullScreenLoadingRequest(label: String) -> UserIndicatorRequest {
let presenter = FullscreenLoadingViewPresenter(
label: label,
- presentingViewController: presentingViewController
+ presentationContext: presentationContext
)
return UserIndicatorRequest(
presenter: presenter,
@@ -116,7 +109,7 @@ class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol {
style: .success,
label: label
),
- presentingViewController: presentingViewController
+ presentationContext: presentationContext
)
return UserIndicatorRequest(
presenter: presenter,
diff --git a/Riot/Modules/Common/UserIndicators/ViewPresenters/FullscreenLoadingViewPresenter.swift b/Riot/Modules/Common/UserIndicators/ViewPresenters/FullscreenLoadingViewPresenter.swift
index 5532bcb2a..d260a0497 100644
--- a/Riot/Modules/Common/UserIndicators/ViewPresenters/FullscreenLoadingViewPresenter.swift
+++ b/Riot/Modules/Common/UserIndicators/ViewPresenters/FullscreenLoadingViewPresenter.swift
@@ -22,18 +22,18 @@ import UIKit
/// It is managed by a `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes.
class FullscreenLoadingViewPresenter: UserIndicatorViewPresentable {
private let label: String
- private weak var viewController: UIViewController?
+ private let presentationContext: UserIndicatorPresentationContext
private weak var view: UIView?
private var animator: UIViewPropertyAnimator?
- init(label: String, presentingViewController: UIViewController) {
+ init(label: String, presentationContext: UserIndicatorPresentationContext) {
self.label = label
- self.viewController = presentingViewController
+ self.presentationContext = presentationContext
}
func present() {
// Find the current top navigation controller
- var presentingController: UIViewController? = viewController
+ var presentingController: UIViewController? = presentationContext.indicatorPresentingViewController
while presentingController?.navigationController != nil {
presentingController = presentingController?.navigationController
}
diff --git a/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift b/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift
index b56e511d2..a54e37a41 100644
--- a/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift
+++ b/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift
@@ -27,17 +27,17 @@ class ToastViewPresenter: UserIndicatorViewPresentable {
}
private let viewState: ToastViewState
- private weak var viewController: UIViewController?
+ private let presentationContext: UserIndicatorPresentationContext
private weak var view: UIView?
private var animator: UIViewPropertyAnimator?
- init(viewState: ToastViewState, presentingViewController: UIViewController) {
+ init(viewState: ToastViewState, presentationContext: UserIndicatorPresentationContext) {
self.viewState = viewState
- self.viewController = presentingViewController
+ self.presentationContext = presentationContext
}
func present() {
- guard let viewController = viewController else {
+ guard let viewController = presentationContext.indicatorPresentingViewController else {
return
}
diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m
index bc1815a41..76b161caf 100644
--- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m
+++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m
@@ -801,6 +801,7 @@
if (indexPath.row < directChatsArray.count)
{
// Open this room
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerSearchContactDetail;
[[AppDelegate theDelegate] showRoom:directChatsArray[indexPath.row] andEventId:nil withMatrixSession:self.mainSession];
}
else
@@ -1053,7 +1054,8 @@
self->roomCreationRequest = nil;
[self removePendingActionMask];
-
+
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated;
[[AppDelegate theDelegate] showRoom:room.roomId andEventId:nil withMatrixSession:self.mainSession];
} failure:onFailure];
diff --git a/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift b/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift
index e268d9528..b86728e93 100644
--- a/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift
+++ b/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift
@@ -18,6 +18,21 @@
import UIKit
+@objcMembers
+class CreateRoomCoordinatorParameter: NSObject {
+ /// Instance of the current MXSession
+ let session: MXSession
+
+ /// Instance of the parent space. `nil` if home space
+ let parentSpace: MXSpace?
+
+ init(session: MXSession,
+ parentSpace: MXSpace?) {
+ self.session = session
+ self.parentSpace = parentSpace
+ }
+}
+
@objcMembers
final class CreateRoomCoordinator: CreateRoomCoordinatorType {
@@ -26,7 +41,8 @@ final class CreateRoomCoordinator: CreateRoomCoordinatorType {
// MARK: Private
private let navigationRouter: NavigationRouterType
- private let session: MXSession
+ private let tabRouter: TabbedRouterType
+ private let parameters: CreateRoomCoordinatorParameter
// MARK: Public
@@ -35,25 +51,56 @@ final class CreateRoomCoordinator: CreateRoomCoordinatorType {
weak var delegate: CreateRoomCoordinatorDelegate?
+ var parentSpace: MXSpace? {
+ parameters.parentSpace
+ }
+
// MARK: - Setup
- init(session: MXSession) {
+ init(parameters: CreateRoomCoordinatorParameter) {
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
- self.session = session
+ let segmentedController = SegmentedController.instantiate()
+ segmentedController.title = VectorL10n.spacesAddRoom
+ self.tabRouter = SegmentedRouter(segmentedController: segmentedController)
+ self.parameters = parameters
}
// MARK: - Public methods
func start() {
- let rootCoordinator = self.createEnterNewRoomDetailsCoordinator()
+ let createRoomCoordinator = self.createEnterNewRoomDetailsCoordinator()
- rootCoordinator.start()
+ createRoomCoordinator.start()
- self.add(childCoordinator: rootCoordinator)
+ self.add(childCoordinator: createRoomCoordinator)
- self.navigationRouter.setRootModule(rootCoordinator)
- }
+ if let parentSpace = self.parentSpace, #available(iOS 14, *) {
+ let roomSelectionCoordinator = self.createRoomSelectorCoordinator(parentSpace: parentSpace)
+ roomSelectionCoordinator.completion = { [weak self] result in
+ guard let self = self else {
+ return
+ }
+
+ switch result {
+ case .done(let selectedItemIds):
+ self.delegate?.createRoomCoordinator(self, didAddRoomsWithIds: selectedItemIds)
+ default:
+ self.delegate?.createRoomCoordinatorDidCancel(self)
+ }
+ }
+ roomSelectionCoordinator.start()
+ self.add(childCoordinator: roomSelectionCoordinator)
+ self.tabRouter.tabs = [
+ TabbedRouterTab(title: VectorL10n.newWord, icon: nil, module: createRoomCoordinator),
+ TabbedRouterTab(title: VectorL10n.existing, icon: nil, module: roomSelectionCoordinator)
+ ]
+ self.navigationRouter.setRootModule(self.tabRouter)
+ Analytics.shared.exploringSpace = parentSpace
+ } else {
+ self.navigationRouter.setRootModule(createRoomCoordinator)
+ }
+ }
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
@@ -62,10 +109,17 @@ final class CreateRoomCoordinator: CreateRoomCoordinatorType {
// MARK: - Private methods
private func createEnterNewRoomDetailsCoordinator() -> EnterNewRoomDetailsCoordinator {
- let coordinator = EnterNewRoomDetailsCoordinator(session: self.session)
+ let coordinator = EnterNewRoomDetailsCoordinator(session: self.parameters.session, parentSpace: self.parentSpace)
coordinator.delegate = self
return coordinator
}
+
+ @available(iOS 14.0, *)
+ private func createRoomSelectorCoordinator(parentSpace: MXSpace) -> MatrixItemChooserCoordinator {
+ let paramaters = MatrixItemChooserCoordinatorParameters(session: self.parameters.session, viewProvider: AddRoomSelectorViewProvider(), itemsProcessor: AddRoomItemsProcessor(parentSpace: parentSpace))
+ let coordinator = MatrixItemChooserCoordinator(parameters: paramaters)
+ return coordinator
+ }
}
// MARK: - EnterNewRoomDetailsCoordinatorDelegate
diff --git a/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift b/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift
index 44bc7d2b8..24efedb4c 100644
--- a/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift
+++ b/Riot/Modules/CreateRoom/CreateRoomCoordinatorBridgePresenter.swift
@@ -20,6 +20,7 @@ import Foundation
@objc protocol CreateRoomCoordinatorBridgePresenterDelegate {
func createRoomCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: CreateRoomCoordinatorBridgePresenter, didCreateNewRoom room: MXRoom)
+ func createRoomCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: CreateRoomCoordinatorBridgePresenter, didAddRoomsWithIds roomIds: [String])
func createRoomCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: CreateRoomCoordinatorBridgePresenter)
}
@@ -32,7 +33,7 @@ final class CreateRoomCoordinatorBridgePresenter: NSObject {
// MARK: Private
- private let session: MXSession
+ private let parameters: CreateRoomCoordinatorParameter
private var coordinator: CreateRoomCoordinator?
// MARK: Public
@@ -41,8 +42,8 @@ final class CreateRoomCoordinatorBridgePresenter: NSObject {
// MARK: - Setup
- init(session: MXSession) {
- self.session = session
+ init(parameters: CreateRoomCoordinatorParameter) {
+ self.parameters = parameters
super.init()
}
@@ -54,7 +55,7 @@ final class CreateRoomCoordinatorBridgePresenter: NSObject {
// }
func present(from viewController: UIViewController, animated: Bool) {
- let createRoomCoordinator = CreateRoomCoordinator(session: self.session)
+ let createRoomCoordinator = CreateRoomCoordinator(parameters: self.parameters)
createRoomCoordinator.delegate = self
let presentable = createRoomCoordinator.toPresentable()
presentable.presentationController?.delegate = self
@@ -86,6 +87,10 @@ extension CreateRoomCoordinatorBridgePresenter: CreateRoomCoordinatorDelegate {
self.delegate?.createRoomCoordinatorBridgePresenterDelegate(self, didCreateNewRoom: room)
}
+ func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) {
+ self.delegate?.createRoomCoordinatorBridgePresenterDelegate(self, didAddRoomsWithIds: roomIds)
+ }
+
func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) {
self.delegate?.createRoomCoordinatorBridgePresenterDelegateDidCancel(self)
}
diff --git a/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift b/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift
index 585538ddb..8c1b36c35 100644
--- a/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift
+++ b/Riot/Modules/CreateRoom/CreateRoomCoordinatorType.swift
@@ -20,10 +20,12 @@ import Foundation
protocol CreateRoomCoordinatorDelegate: AnyObject {
func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom)
+ func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String])
func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType)
}
/// `CreateRoomCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow.
protocol CreateRoomCoordinatorType: Coordinator, Presentable {
var delegate: CreateRoomCoordinatorDelegate? { get }
+ var parentSpace: MXSpace? { get }
}
diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift
index f310f442c..9a03641f7 100644
--- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift
+++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsCoordinator.swift
@@ -26,6 +26,7 @@ final class EnterNewRoomDetailsCoordinator: EnterNewRoomDetailsCoordinatorType {
// MARK: Private
private let session: MXSession
+ private let parentSpace: MXSpace?
private var enterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType
private let enterNewRoomDetailsViewController: EnterNewRoomDetailsViewController
@@ -44,10 +45,11 @@ final class EnterNewRoomDetailsCoordinator: EnterNewRoomDetailsCoordinatorType {
// MARK: - Setup
- init(session: MXSession) {
+ init(session: MXSession, parentSpace: MXSpace?) {
self.session = session
+ self.parentSpace = parentSpace
- let enterNewRoomDetailsViewModel = EnterNewRoomDetailsViewModel(session: self.session)
+ let enterNewRoomDetailsViewModel = EnterNewRoomDetailsViewModel(session: self.session, parentSpace: self.parentSpace)
let enterNewRoomDetailsViewController = EnterNewRoomDetailsViewController.instantiate(with: enterNewRoomDetailsViewModel)
self.enterNewRoomDetailsViewModel = enterNewRoomDetailsViewModel
self.enterNewRoomDetailsViewController = enterNewRoomDetailsViewController
diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift
index 00511c65a..0ea21a4b3 100644
--- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift
+++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift
@@ -49,7 +49,14 @@ final class EnterNewRoomDetailsViewController: UIViewController {
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private lazy var createBarButtonItem: MXKBarButtonItem = {
- let item = MXKBarButtonItem(title: VectorL10n.create, style: .plain) { [weak self] in
+ let title: String
+ switch viewModel.actionType {
+ case .createAndAddToSpace:
+ title = VectorL10n.add
+ case .createOnly:
+ title = VectorL10n.create
+ }
+ let item = MXKBarButtonItem(title: title, style: .plain) { [weak self] in
self?.createButtonAction()
}!
item.isEnabled = false
@@ -120,30 +127,59 @@ final class EnterNewRoomDetailsViewController: UIViewController {
var section4: Section?
if RiotSettings.shared.roomCreationScreenAllowRoomTypeConfiguration {
- let row_4_0 = Row(type: .default, text: VectorL10n.createRoomTypePrivate, accessoryType: viewModel.roomCreationParameters.isPublic ? .none : .checkmark) { [weak self] in
+ let row_4_0 = Row(type: .default, text: VectorL10n.createRoomTypePrivate, accessoryType: viewModel.roomCreationParameters.joinRule == .private ? .checkmark : .none) { [weak self] in
guard let self = self else {
return
}
- self.viewModel.roomCreationParameters.isPublic = false
+ self.viewModel.roomCreationParameters.joinRule = .private
self.updateSections()
}
- let row_4_1 = Row(type: .default, text: VectorL10n.createRoomTypePublic, accessoryType: viewModel.roomCreationParameters.isPublic ? .checkmark : .none) { [weak self] in
+ let row_4_1 = Row(type: .default, text: VectorL10n.createRoomTypeRestricted, accessoryType: viewModel.roomCreationParameters.joinRule == .restricted ? .checkmark : .none) { [weak self] in
guard let self = self else {
return
}
- self.viewModel.roomCreationParameters.isPublic = true
+ self.viewModel.roomCreationParameters.joinRule = .restricted
+ self.updateSections()
+ // scroll bottom to show user new fields
+ DispatchQueue.main.async {
+ self.mainTableView.scrollToRow(at: IndexPath(row: 0, section: 5), at: .bottom, animated: true)
+ }
+ }
+ let row_4_2 = Row(type: .default, text: VectorL10n.createRoomTypePublic, accessoryType: viewModel.roomCreationParameters.joinRule == .public ? .checkmark : .none) { [weak self] in
+
+ guard let self = self else {
+ return
+ }
+
+ self.viewModel.roomCreationParameters.joinRule = .public
self.updateSections()
// scroll bottom to show user new fields
DispatchQueue.main.async {
self.mainTableView.scrollToRow(at: IndexPath(row: 0, section: 6), at: .bottom, animated: true)
}
}
+ let rows: [Row]
+ switch viewModel.actionType {
+ case .createAndAddToSpace:
+ rows = [row_4_0, row_4_1, row_4_2]
+ case .createOnly:
+ rows = [row_4_0, row_4_2]
+ }
+ let footer: String
+ switch viewModel.roomCreationParameters.joinRule {
+ case .private:
+ footer = VectorL10n.createRoomSectionFooterTypePrivate
+ case .restricted:
+ footer = VectorL10n.createRoomSectionFooterTypeRestricted
+ default:
+ footer = VectorL10n.createRoomSectionFooterTypePublic
+ }
section4 = Section(header: VectorL10n.createRoomSectionHeaderType,
- rows: [row_4_0, row_4_1],
- footer: VectorL10n.createRoomSectionFooterType)
+ rows: rows,
+ footer: footer)
}
var tmpSections: [Section] = [
@@ -160,15 +196,28 @@ final class EnterNewRoomDetailsViewController: UIViewController {
tmpSections.append(section4)
}
- if viewModel.roomCreationParameters.isPublic {
+ if viewModel.roomCreationParameters.joinRule == .public {
let row_5_0 = Row(type: .withSwitch(isOn: viewModel.roomCreationParameters.showInDirectory, onValueChanged: { [weak self] (theSwitch) in
self?.viewModel.roomCreationParameters.showInDirectory = theSwitch.isOn
}), text: VectorL10n.createRoomShowInDirectory, accessoryType: .none) {
// no-op
}
- let section5 = Section(header: nil,
- rows: [row_5_0],
- footer: nil)
+
+ let rows: [Row]
+ if viewModel.actionType == .createAndAddToSpace {
+ let row_5_1 = Row(type: .withSwitch(isOn: viewModel.roomCreationParameters.isRoomSuggested, onValueChanged: { [weak self] (theSwitch) in
+ self?.viewModel.roomCreationParameters.isRoomSuggested = theSwitch.isOn
+ }), text: VectorL10n.createRoomSuggestRoom, accessoryType: .none) {
+ // no-op
+ }
+ rows = [row_5_0, row_5_1]
+ } else {
+ rows = [row_5_0]
+ }
+
+ let section5 = Section(header: VectorL10n.createRoomPromotionHeader,
+ rows: rows,
+ footer: VectorL10n.createRoomShowInDirectoryFooter)
let row_6_0 = Row(type: .textField(tag: Constants.roomAddressTextFieldTag, placeholder: VectorL10n.createRoomPlaceholderAddress, delegate: self), text: viewModel.roomCreationParameters.address, accessoryType: .none) {
@@ -180,6 +229,18 @@ final class EnterNewRoomDetailsViewController: UIViewController {
tmpSections.append(contentsOf: [section5, section6])
}
+ if viewModel.roomCreationParameters.joinRule == .restricted {
+ let row_5_0 = Row(type: .withSwitch(isOn: viewModel.roomCreationParameters.isRoomSuggested, onValueChanged: { [weak self] (theSwitch) in
+ self?.viewModel.roomCreationParameters.isRoomSuggested = theSwitch.isOn
+ }), text: VectorL10n.createRoomSuggestRoom, accessoryType: .none) {
+ // no-op
+ }
+ let section5 = Section(header: VectorL10n.createRoomPromotionHeader,
+ rows: [row_5_0],
+ footer: VectorL10n.createRoomSuggestRoomFooter)
+ tmpSections.append(section5)
+ }
+
sections = tmpSections
}
diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift
index 407e0366b..3a518e846 100644
--- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift
+++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift
@@ -25,6 +25,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType {
// MARK: Private
private let session: MXSession
+ private let parentSpace: MXSpace?
private var currentOperation: MXHTTPOperation?
private var mediaUploader: MXMediaLoader?
@@ -41,12 +42,17 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType {
}
}
+ var actionType: EnterNewRoomActionType {
+ parentSpace != nil ? .createAndAddToSpace : .createOnly
+ }
+
// MARK: - Setup
- init(session: MXSession) {
+ init(session: MXSession, parentSpace: MXSpace?) {
self.session = session
- roomCreationParameters.isEncrypted = session.vc_homeserverConfiguration().isE2EEByDefaultEnabled && RiotSettings.shared.roomCreationScreenRoomIsEncrypted
- roomCreationParameters.isPublic = RiotSettings.shared.roomCreationScreenRoomIsPublic
+ self.parentSpace = parentSpace
+ roomCreationParameters.isEncrypted = session.vc_homeserverConfiguration().encryption.isE2EEByDefaultEnabled && RiotSettings.shared.roomCreationScreenRoomIsEncrypted
+ roomCreationParameters.joinRule = RiotSettings.shared.roomCreationScreenRoomIsPublic ? .public : .private
viewState = .loaded
}
@@ -105,33 +111,37 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType {
}
private func createRoom() {
- // compose room creation parameters in Matrix level
- let parameters = MXRoomCreationParameters()
- parameters.name = roomCreationParameters.name
- parameters.topic = roomCreationParameters.topic
- parameters.roomAlias = fixRoomAlias(alias: roomCreationParameters.address)
-
- if roomCreationParameters.isPublic {
- parameters.preset = kMXRoomPresetPublicChat
- if roomCreationParameters.showInDirectory {
- parameters.visibility = kMXRoomDirectoryVisibilityPublic
- } else {
- parameters.visibility = kMXRoomDirectoryVisibilityPrivate
- }
- } else {
- parameters.preset = kMXRoomPresetPrivateChat
- parameters.visibility = kMXRoomDirectoryVisibilityPrivate
+ guard let roomName = roomCreationParameters.name else {
+ fatalError("[EnterNewRoomDetailsViewModel] createRoom: room name cannot be nil.")
}
- if roomCreationParameters.isEncrypted {
- parameters.initialStateEvents = [MXRoomCreationParameters.initialStateEventForEncryption(withAlgorithm: kMXCryptoMegolmAlgorithm)]
- }
-
- viewState = .loading
-
- currentOperation = session.createRoom(parameters: parameters) { (response) in
+ currentOperation = session.createRoom(
+ withName: roomName,
+ joinRule: roomCreationParameters.joinRule,
+ topic: roomCreationParameters.topic,
+ parentRoomId: parentSpace?.spaceId,
+ aliasLocalPart: fixRoomAlias(alias: roomCreationParameters.address),
+ isEncrypted: roomCreationParameters.isEncrypted,
+ completion: { response in
+ switch response {
+ case .success(let room):
+ if let parentSpace = self.parentSpace {
+ self.add(room, to: parentSpace)
+ } else {
+ self.uploadAvatarIfRequired(ofRoom: room)
+ self.currentOperation = nil
+ }
+ case .failure(let error):
+ self.viewState = .error(error)
+ self.currentOperation = nil
+ }
+ })
+ }
+
+ private func add(_ room: MXRoom, to space: MXSpace) {
+ currentOperation = space.addChild(roomId: room.roomId, suggested: roomCreationParameters.isRoomSuggested) { response in
switch response {
- case .success(let room):
+ case .success:
self.uploadAvatarIfRequired(ofRoom: room)
self.currentOperation = nil
case .failure(let error):
diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift
index 5ad3623ec..95ccbcaa5 100644
--- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift
+++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift
@@ -28,6 +28,11 @@ protocol EnterNewRoomDetailsViewModelCoordinatorDelegate: AnyObject {
func enterNewRoomDetailsViewModelDidCancel(_ viewModel: EnterNewRoomDetailsViewModelType)
}
+enum EnterNewRoomActionType {
+ case createOnly
+ case createAndAddToSpace
+}
+
/// Protocol describing the view model used by `EnterNewRoomDetailsViewController`
protocol EnterNewRoomDetailsViewModelType {
@@ -39,4 +44,6 @@ protocol EnterNewRoomDetailsViewModelType {
var roomCreationParameters: RoomCreationParameters { get set }
var viewState: EnterNewRoomDetailsViewState { get }
+
+ var actionType: EnterNewRoomActionType { get }
}
diff --git a/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift b/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift
index c84f8eb27..311234c27 100644
--- a/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift
+++ b/Riot/Modules/CreateRoom/Models/RoomCreationParameters.swift
@@ -24,16 +24,22 @@ struct RoomCreationParameters {
return userSelectedAvatar
}
var isEncrypted: Bool = false
- var isPublic: Bool = false {
+ var joinRule: MXRoomJoinRule = .private {
didSet {
- if !isPublic {
- // if set to private again, reset some fields
+ switch joinRule {
+ case .restricted:
showInDirectory = false
address = nil
+ case .private:
+ showInDirectory = false
+ address = nil
+ isRoomSuggested = false
+ default: break
}
}
}
var showInDirectory: Bool = false
+ var isRoomSuggested: Bool = false
var userSelectedAvatar: UIImage?
}
diff --git a/Riot/Modules/Favorites/FavouritesViewController.m b/Riot/Modules/Favorites/FavouritesViewController.m
index eb9499359..49942eeed 100644
--- a/Riot/Modules/Favorites/FavouritesViewController.m
+++ b/Riot/Modules/Favorites/FavouritesViewController.m
@@ -130,9 +130,13 @@
[self.tableViewPaginationThrottler throttle:^{
NSInteger section = indexPath.section;
+ if (tableView.numberOfSections <= section)
+ {
+ return;
+ }
+
NSInteger numberOfRowsInSection = [tableView numberOfRowsInSection:section];
- if (tableView.numberOfSections > section
- && indexPath.row == numberOfRowsInSection - 1)
+ if (indexPath.row == numberOfRowsInSection - 1)
{
[self->recentsDataSource paginateInSection:section];
}
diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m
index 84ab44030..04297d759 100644
--- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m
+++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m
@@ -164,7 +164,7 @@
mxSession:session
threadParameters:threadParameters
presentationParameters:presentationParameters];
-
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerFileSearch;
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
}
diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m
index c3c42fb07..ea0c6a2f3 100644
--- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m
+++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m
@@ -171,6 +171,7 @@
mxSession:self.mainSession
threadParameters:threadParameters
presentationParameters:screenParameters];
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerMessageSearch;
[[LegacyAppDelegate theDelegate] showRoomWithParameters:parameters];
}
diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m
index e0ab0f218..876d80cab 100644
--- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m
+++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m
@@ -243,11 +243,13 @@
mxSession:mxSession
threadParameters:nil
presentationParameters:presentationParameters];
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomDirectory;
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
}
- (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData
{
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerRoomDirectory;
ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO];
RoomPreviewNavigationParameters *parameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:presentationParameters];
diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m
index c627650af..77fd9f012 100644
--- a/Riot/Modules/Home/HomeViewController.m
+++ b/Riot/Modules/Home/HomeViewController.m
@@ -108,13 +108,11 @@
[AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = ThemeService.shared.theme.tintColor;
- if (recentsDataSource)
+ if (recentsDataSource.recentsDataSourceMode != RecentsDataSourceModeHome)
{
// Take the lead on the shared data source.
[recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeHome];
- }
-
- [self moveAllCollectionsToLeft];
+ }
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator
@@ -133,26 +131,6 @@
[super destroy];
}
-- (void)moveAllCollectionsToLeft
-{
- selectedCollectionViewContentOffset = -1;
-
- // Scroll all rooms collections to their beginning
- for (NSInteger section = 0; section < [self numberOfSectionsInTableView:self.recentsTableView]; section++)
- {
- UITableViewCell *firstSectionCell = [self.recentsTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]];
- if (firstSectionCell && [firstSectionCell isKindOfClass:TableViewCellWithCollectionView.class])
- {
- TableViewCellWithCollectionView *tableViewCell = (TableViewCellWithCollectionView*)firstSectionCell;
-
- if ([tableViewCell.collectionView numberOfItemsInSection:0] > 0)
- {
- [tableViewCell.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
- }
- }
- }
-}
-
- (SecureBackupBannerCell *)secureBackupBannerPrototypeCell
{
if (!_secureBackupBannerPrototypeCell)
@@ -255,72 +233,7 @@
[self cancelEditionMode:YES];
}
- if (recentsDataSource.currentSpace != nil)
- {
- [self showPlusMenuForSpace];
- }
- else
- {
- [super onPlusButtonPressed];
- }
-}
-
-- (void)showPlusMenuForSpace
-{
- __weak typeof(self) weakSelf = self;
-
- [currentAlert dismissViewControllerAnimated:NO completion:nil];
-
- currentAlert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
-
- [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n spacesExploreRooms]
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self->currentAlert = nil;
-
- [self showRoomDirectory];
- }
-
- }]];
-
- [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomDetailsPeople]
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self->currentAlert = nil;
-
- self.spaceMembersCoordinatorBridgePresenter = [[SpaceMembersCoordinatorBridgePresenter alloc] initWithUserSessionsService:[UserSessionsService shared] session:self.mainSession spaceId:self.dataSource.currentSpace.spaceId];
- self.spaceMembersCoordinatorBridgePresenter.delegate = self;
- [self.spaceMembersCoordinatorBridgePresenter presentFrom:self animated:YES];
- }
-
- }]];
-
-
- [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
- style:UIAlertActionStyleCancel
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self->currentAlert = nil;
- }
-
- }]];
-
- [currentAlert popoverPresentationController].sourceView = plusButtonImageView;
- [currentAlert popoverPresentationController].sourceRect = plusButtonImageView.bounds;
-
- [currentAlert mxk_setAccessibilityIdentifier:@"RecentsVCCreateRoomAlert"];
- [self presentViewController:currentAlert animated:YES completion:nil];
+ [super onPlusButtonPressed];
}
- (void)cancelEditionMode:(BOOL)forceRefresh
@@ -358,6 +271,35 @@
[self updateEmptyView];
}
+- (void)startChat {
+ if (recentsDataSource.currentSpace)
+ {
+ self.spaceMembersCoordinatorBridgePresenter = [[SpaceMembersCoordinatorBridgePresenter alloc] initWithUserSessionsService:[UserSessionsService shared] session:self.mainSession spaceId:self.dataSource.currentSpace.spaceId];
+ self.spaceMembersCoordinatorBridgePresenter.delegate = self;
+ [self.spaceMembersCoordinatorBridgePresenter presentFrom:self animated:YES];
+ }
+ else
+ {
+ [super startChat];
+ }
+}
+
+- (void)createNewRoom
+{
+ if (recentsDataSource.currentSpace) {
+ [recentsDataSource.currentSpace canAddRoomWithCompletion:^(BOOL canAddRoom) {
+ if (canAddRoom) {
+ [super createNewRoom];
+ } else {
+ [[AppDelegate theDelegate] showAlertWithTitle:[VectorL10n roomRecentsCreateEmptyRoom]
+ message:[VectorL10n spacesAddRoomMissingPermissionMessage]];
+ }
+ }];
+ } else {
+ [super createNewRoom];
+ }
+}
+
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
@@ -636,9 +578,13 @@
{
[self.collectionViewPaginationThrottler throttle:^{
NSInteger collectionViewSection = indexPath.section;
+ if (collectionView.numberOfSections <= collectionViewSection)
+ {
+ return;
+ }
+
NSInteger numberOfItemsInSection = [collectionView numberOfItemsInSection:collectionViewSection];
- if (collectionView.numberOfSections > collectionViewSection
- && indexPath.item == numberOfItemsInSection - 1)
+ if (indexPath.item == numberOfItemsInSection - 1)
{
NSInteger tableViewSection = collectionView.tag;
[self->recentsDataSource paginateInSection:tableViewSection];
diff --git a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift
index 3fce9171d..c14c56272 100644
--- a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift
+++ b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift
@@ -92,7 +92,7 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType {
self.createKeyBackupUsingSecureBackup(privateKey: privateKey, completion: completion)
}
- let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: .passphraseOrKey, recoveryGoal: recoveryGoal, navigationRouter: self.navigationRouter)
+ let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: .passphraseOrKey, recoveryGoal: recoveryGoal, navigationRouter: self.navigationRouter, cancellable: true)
coordinator.delegate = self
coordinator.start()
self.add(childCoordinator: coordinator)
diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift
index 19dbc98f8..d7b31c693 100644
--- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift
+++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift
@@ -29,6 +29,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType {
private let session: MXSession
private let verificationFlow: KeyVerificationFlow
private let verificationKind: KeyVerificationKind
+ private let cancellable: Bool
private weak var completeSecurityCoordinator: KeyVerificationSelfVerifyWaitCoordinatorType?
private var otherUserId: String {
@@ -86,7 +87,8 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType {
/// - session: The MXSession.
/// - flow: The wanted key verification flow.
/// - navigationRouter: Existing NavigationRouter from which present the flow (optional).
- init(session: MXSession, flow: KeyVerificationFlow, navigationRouter: NavigationRouterType? = nil) {
+ /// - cancellable: Whether key verification process can be cancelled.
+ init(session: MXSession, flow: KeyVerificationFlow, navigationRouter: NavigationRouterType? = nil, cancellable: Bool) {
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
self.session = session
@@ -113,6 +115,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType {
}
self.verificationKind = verificationKind
+ self.cancellable = cancellable
}
// MARK: - Public methods
@@ -155,7 +158,9 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType {
}
func toPresentable() -> UIViewController {
- return self.navigationRouter.toPresentable()
+ return self.navigationRouter
+ .toPresentable()
+ .vc_setModalFullScreen(!self.cancellable)
}
// MARK: - Private methods
@@ -177,7 +182,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType {
}
private func createCompleteSecurityCoordinator(isNewSignIn: Bool) -> KeyVerificationSelfVerifyWaitCoordinatorType {
- let coordinator = KeyVerificationSelfVerifyWaitCoordinator(session: self.session, isNewSignIn: isNewSignIn)
+ let coordinator = KeyVerificationSelfVerifyWaitCoordinator(session: self.session, isNewSignIn: isNewSignIn, cancellable: self.cancellable)
coordinator.delegate = self
coordinator.start()
@@ -185,7 +190,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType {
}
private func showSecretsRecovery(with recoveryMode: SecretsRecoveryMode) {
- let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: recoveryMode, recoveryGoal: .verifyDevice, navigationRouter: self.navigationRouter)
+ let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: recoveryMode, recoveryGoal: .verifyDevice, navigationRouter: self.navigationRouter, cancellable: self.cancellable)
coordinator.delegate = self
coordinator.start()
diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift
index 5d8ead04c..f07ea75a2 100644
--- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift
+++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift
@@ -38,6 +38,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject {
// MARK: Public
weak var delegate: KeyVerificationCoordinatorBridgePresenterDelegate?
+ var cancellable: Bool = true
var isPresenting: Bool {
return self.coordinator != nil
@@ -61,7 +62,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject {
MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present from \(viewController)")
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: otherUserId, deviceId: otherDeviceId))
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: otherUserId, deviceId: otherDeviceId), cancellable: self.cancellable)
self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated)
}
@@ -69,7 +70,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject {
MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present from \(viewController)")
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyUser(roomMember))
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyUser(roomMember), cancellable: self.cancellable)
self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated)
}
@@ -77,7 +78,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject {
MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present incoming verification from \(viewController)")
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingSASTransaction(incomingTransaction))
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingSASTransaction(incomingTransaction), cancellable: self.cancellable)
self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated)
}
@@ -85,7 +86,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject {
MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present incoming key verification request from \(viewController)")
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingRequest(incomingKeyVerificationRequest))
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingRequest(incomingKeyVerificationRequest), cancellable: self.cancellable)
self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated)
}
@@ -93,7 +94,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject {
MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present complete security from \(viewController)")
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn))
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), cancellable: self.cancellable)
self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated)
}
@@ -103,7 +104,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject {
let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController)
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), navigationRouter: navigationRouter)
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), navigationRouter: navigationRouter, cancellable: self.cancellable)
keyVerificationCoordinator.delegate = self
keyVerificationCoordinator.start() // Will trigger view controller push
diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift
index 3b6e2824a..94b3a3eca 100644
--- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift
+++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift
@@ -28,6 +28,7 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW
private let session: MXSession
private var keyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWaitViewModelType
private let keyVerificationSelfVerifyWaitViewController: KeyVerificationSelfVerifyWaitViewController
+ private let cancellable: Bool
// MARK: Public
@@ -38,13 +39,14 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW
// MARK: - Setup
- init(session: MXSession, isNewSignIn: Bool) {
+ init(session: MXSession, isNewSignIn: Bool, cancellable: Bool) {
self.session = session
let keyVerificationSelfVerifyWaitViewModel = KeyVerificationSelfVerifyWaitViewModel(session: self.session, isNewSignIn: isNewSignIn)
- let keyVerificationSelfVerifyWaitViewController = KeyVerificationSelfVerifyWaitViewController.instantiate(with: keyVerificationSelfVerifyWaitViewModel)
+ let keyVerificationSelfVerifyWaitViewController = KeyVerificationSelfVerifyWaitViewController.instantiate(with: keyVerificationSelfVerifyWaitViewModel, cancellable: cancellable)
self.keyVerificationSelfVerifyWaitViewModel = keyVerificationSelfVerifyWaitViewModel
self.keyVerificationSelfVerifyWaitViewController = keyVerificationSelfVerifyWaitViewController
+ self.cancellable = cancellable
}
// MARK: - Public methods
@@ -55,6 +57,7 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW
func toPresentable() -> UIViewController {
return self.keyVerificationSelfVerifyWaitViewController
+ .vc_setModalFullScreen(!self.cancellable)
}
}
diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift
index dd988c45e..f7969bf22 100644
--- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift
+++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift
@@ -47,6 +47,7 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController {
// MARK: Private
private var viewModel: KeyVerificationSelfVerifyWaitViewModelType!
+ private var cancellable: Bool!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
@@ -55,9 +56,10 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController {
// MARK: - Setup
- class func instantiate(with viewModel: KeyVerificationSelfVerifyWaitViewModelType) -> KeyVerificationSelfVerifyWaitViewController {
+ class func instantiate(with viewModel: KeyVerificationSelfVerifyWaitViewModelType, cancellable: Bool) -> KeyVerificationSelfVerifyWaitViewController {
let viewController = StoryboardScene.KeyVerificationSelfVerifyWaitViewController.initialScene.instantiate()
viewController.viewModel = viewModel
+ viewController.cancellable = cancellable
viewController.theme = ThemeService.shared().theme
return viewController
}
@@ -112,15 +114,17 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController {
}
private func setupViews() {
- let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.skip, style: .plain) { [weak self] in
- self?.cancelButtonAction()
+ if self.cancellable {
+ let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.skip, style: .plain) { [weak self] in
+ self?.cancelButtonAction()
+ }
+
+ self.vc_removeBackTitle()
+
+ self.navigationItem.rightBarButtonItem = cancelBarButtonItem
+ self.cancelBarButtonItem = cancelBarButtonItem
}
- self.vc_removeBackTitle()
-
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
- self.cancelBarButtonItem = cancelBarButtonItem
-
self.title = VectorL10n.deviceVerificationSelfVerifyWaitTitle
self.informationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitInformation(AppInfo.current.displayName)
diff --git a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift
index 55565fb9d..b56748a40 100644
--- a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift
+++ b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift
@@ -118,7 +118,7 @@ final class UserVerificationCoordinator: NSObject, UserVerificationCoordinatorTy
private func presentDeviceVerification(for deviceId: String) {
- let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: self.userId, deviceId: deviceId), navigationRouter: self.navigationRouter)
+ let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: self.userId, deviceId: deviceId), navigationRouter: self.navigationRouter, cancellable: true)
keyVerificationCoordinator.delegate = self
keyVerificationCoordinator.start()
diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRecentListViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRecentListViewController.m
index 25b50ee61..7285ef564 100644
--- a/Riot/Modules/MatrixKit/Controllers/MXKRecentListViewController.m
+++ b/Riot/Modules/MatrixKit/Controllers/MXKRecentListViewController.m
@@ -169,9 +169,6 @@
// Observe the server sync
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSyncNotification) name:kMXSessionDidSyncNotification object:nil];
-
- // Do a full reload
- [self refreshRecentsTable];
}
- (void)viewWillDisappear:(BOOL)animated
@@ -180,12 +177,6 @@
// The user may still press search button whereas the view disappears
ignoreSearchRequest = YES;
-
- // Leave potential search session
- if (!self.recentsSearchBar.isHidden)
- {
- [self searchBarCancelButtonClicked:self.recentsSearchBar];
- }
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidSyncNotification object:nil];
diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m
index d0047f5f0..728a90fa1 100644
--- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m
+++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m
@@ -2073,6 +2073,11 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes;
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
diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m
index 1a2466a51..e0664c9dd 100644
--- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m
+++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m
@@ -1889,7 +1889,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
id stringLocalizer = [MXKSendReplyEventStringLocalizer new];
- [_room sendReplyToEvent:eventToReply withTextMessage:sanitizedText formattedTextMessage:html stringLocalizer:stringLocalizer localEcho:&localEchoEvent success:success failure:failure];
+ [_room sendReplyToEvent:eventToReply withTextMessage:sanitizedText formattedTextMessage:html stringLocalizer:stringLocalizer threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
if (localEchoEvent)
{
diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
index 3ebb69a4b..2e196bc59 100644
--- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
+++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
@@ -1248,24 +1248,37 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
}
else
{
+ NSDictionary *contentToUse;
+
+ if (event.content[kMXMessageContentKeyNewContent])
+ {
+ // use new content if exists
+ contentToUse = event.content[kMXMessageContentKeyNewContent];
+ }
+ else
+ {
+ // fallback to default content
+ contentToUse = event.content;
+ }
+
NSString *msgtype;
- MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]);
+ MXJSONModelSetString(msgtype, contentToUse[kMXMessageTypeKey]);
NSString *body;
BOOL isHTML = NO;
NSString *eventThreadId = event.threadId;
// Use the HTML formatted string if provided
- if ([event.content[@"format"] isEqualToString:kMXRoomMessageFormatHTML])
+ if ([contentToUse[@"format"] isEqualToString:kMXRoomMessageFormatHTML])
{
isHTML =YES;
- MXJSONModelSetString(body, event.content[@"formatted_body"]);
+ MXJSONModelSetString(body, contentToUse[@"formatted_body"]);
}
- else if (eventThreadId && !RiotSettings.shared.enableThreads)
+ else if (event.isReplyEvent || (eventThreadId && !RiotSettings.shared.enableThreads))
{
NSString *repliedEventId = event.relatesTo.inReplyTo.eventId ?: eventThreadId;
isHTML = YES;
- MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
+ MXJSONModelSetString(body, contentToUse[kMXMessageBodyKey]);
MXEvent *repliedEvent = [mxSession.store eventWithEventId:repliedEventId
inRoom:event.roomId];
@@ -1281,7 +1294,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
}
else
{
- MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
+ MXJSONModelSetString(body, contentToUse[kMXMessageBodyKey]);
}
if (body)
@@ -1561,7 +1574,14 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
else
{
NSString *body;
- MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
+ if (event.content[kMXMessageContentKeyNewContent])
+ {
+ MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]);
+ }
+ else
+ {
+ MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
+ }
// Check sticker validity
if (![self isSupportedAttachment:event])
@@ -1706,7 +1726,16 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
return [self postRenderAttributedString:str];
}
- NSRange bodyRange = [str.string rangeOfString:event.content[kMXMessageBodyKey]];
+ NSString *body;
+ if (event.content[kMXMessageContentKeyNewContent])
+ {
+ MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]);
+ }
+ else
+ {
+ MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
+ }
+ NSRange bodyRange = [str.string rangeOfString:body];
if (bodyRange.location == NSNotFound)
{
// body not found in the whole string
@@ -1776,7 +1805,16 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
return str;
}
- NSRange bodyRange = [str.string rangeOfString:event.content[kMXMessageBodyKey]];
+ NSString *body;
+ if (event.content[kMXMessageContentKeyNewContent])
+ {
+ MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]);
+ }
+ else
+ {
+ MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
+ }
+ NSRange bodyRange = [str.string rangeOfString:body];
if (bodyRange.location == NSNotFound)
{
// body not found in the whole string
@@ -2067,7 +2105,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
textColor = _errorTextColor;
}
// Check whether the message is highlighted.
- else if (event.mxkIsHighlighted || (!RiotSettings.shared.enableThreads && event.isInThread && [event shouldBeHighlightedInSession:mxSession]))
+ else if (event.mxkIsHighlighted || (mxSession && [event shouldBeHighlightedInSession:mxSession]))
{
textColor = _bingTextColor;
}
@@ -2132,7 +2170,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
{
font = _callNoticesTextFont;
}
- else if (event.mxkIsHighlighted || (!RiotSettings.shared.enableThreads && event.isInThread && [event shouldBeHighlightedInSession:mxSession]))
+ else if (event.mxkIsHighlighted || (mxSession && [event shouldBeHighlightedInSession:mxSession]))
{
font = _bingTextFont;
}
@@ -2143,7 +2181,14 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
else if (!_isForSubtitle && !string && event.eventType == MXEventTypeRoomMessage && (_emojiOnlyTextFont || _singleEmojiTextFont))
{
NSString *message;
- MXJSONModelSetString(message, event.content[kMXMessageBodyKey]);
+ if (event.content[kMXMessageContentKeyNewContent])
+ {
+ MXJSONModelSetString(message, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]);
+ }
+ else
+ {
+ MXJSONModelSetString(message, event.content[kMXMessageBodyKey]);
+ }
if (_emojiOnlyTextFont && [MXKTools isEmojiOnlyString:message])
{
diff --git a/Riot/Modules/MatrixKit/Views/MXKTableViewCell/MXKTableViewCell.m b/Riot/Modules/MatrixKit/Views/MXKTableViewCell/MXKTableViewCell.m
index 081bef6bc..196b8b4d1 100644
--- a/Riot/Modules/MatrixKit/Views/MXKTableViewCell/MXKTableViewCell.m
+++ b/Riot/Modules/MatrixKit/Views/MXKTableViewCell/MXKTableViewCell.m
@@ -24,10 +24,10 @@
// Check whether a nib file is available
NSBundle *mainBundle = [NSBundle mxk_bundleForClass:self.class];
- NSString *path = [mainBundle pathForResource:NSStringFromClass([self class]) ofType:@"nib"];
+ NSString *path = [mainBundle pathForResource:[self className] ofType:@"nib"];
if (path)
{
- return [UINib nibWithNibName:NSStringFromClass([self class]) bundle:mainBundle];
+ return [UINib nibWithNibName:[self className] bundle:mainBundle];
}
return nil;
}
@@ -37,6 +37,11 @@
return NSStringFromClass([self class]);
}
++ (NSString*)className
+{
+ return [NSStringFromClass([self class]) componentsSeparatedByString:@"."].lastObject;
+}
+
- (void)awakeFromNib
{
[super awakeFromNib];
diff --git a/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift b/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift
index 1a1d20169..75135e8b2 100644
--- a/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift
+++ b/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift
@@ -27,6 +27,12 @@ import AVFoundation
@objcMembers
final class SingleImagePickerPresenter: NSObject {
+ // MARK: - Constants
+
+ private enum Constants {
+ static let jpegCompressionQuality: CGFloat = 1.0
+ }
+
// MARK: - Properties
// MARK: Private
@@ -117,8 +123,10 @@ final class SingleImagePickerPresenter: NSObject {
// MARK: - CameraPresenterDelegate
extension SingleImagePickerPresenter: CameraPresenterDelegate {
- func cameraPresenter(_ cameraPresenter: CameraPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) {
- self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: uti)
+ func cameraPresenter(_ cameraPresenter: CameraPresenter, didSelectImage image: UIImage) {
+ if let imageData = image.jpegData(compressionQuality: Constants.jpegCompressionQuality) {
+ self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: MXKUTI.jpeg)
+ }
}
func cameraPresenterDidCancel(_ cameraPresenter: CameraPresenter) {
diff --git a/Riot/Modules/MediaPickerV2/MediaPickerPresenter.swift b/Riot/Modules/MediaPickerV2/MediaPickerPresenter.swift
new file mode 100644
index 000000000..193a4cbee
--- /dev/null
+++ b/Riot/Modules/MediaPickerV2/MediaPickerPresenter.swift
@@ -0,0 +1,110 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 UIKit
+import PhotosUI
+import CommonKit
+
+@available(iOS 14.0, *)
+protocol MediaPickerPresenterDelegate: AnyObject {
+ func mediaPickerPresenter(_ presenter: MediaPickerPresenter, didPickImage image: UIImage)
+ func mediaPickerPresenterDidCancel(_ presenter: MediaPickerPresenter)
+}
+
+/// A picker for photos and videos from the user's photo library on iOS 14+ using the
+/// new `PHPickerViewController` that doesn't require permission to be granted.
+///
+/// **Note:** If you need to support iOS 12 & 13, then you will need to use the older
+/// `MediaPickerCoordinator`/`MediaPickerViewController` instead.
+@available(iOS 14.0, *)
+final class MediaPickerPresenter: NSObject {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private weak var pickerViewController: UIViewController?
+
+ private var indicatorPresenter: UserIndicatorTypePresenterProtocol?
+ private var loadingIndicator: UserIndicator?
+
+ // MARK: Public
+
+ weak var delegate: MediaPickerPresenterDelegate?
+
+ // MARK: - Public
+
+ // TODO: Support videos and multi-selection
+ func presentPicker(from presentingViewController: UIViewController, with filter: PHPickerFilter?, animated: Bool) {
+ var configuration = PHPickerConfiguration(photoLibrary: .shared())
+ configuration.selectionLimit = 1
+ configuration.filter = filter
+
+ let pickerViewController = PHPickerViewController(configuration: configuration)
+ pickerViewController.delegate = self
+
+ self.pickerViewController = pickerViewController
+ indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pickerViewController)
+
+ presentingViewController.present(pickerViewController, animated: true, completion: nil)
+ }
+
+ func dismiss(animated: Bool, completion: (() -> Void)?) {
+ guard let pickerViewController = pickerViewController else { return }
+ pickerViewController.dismiss(animated: animated, completion: completion)
+ }
+
+ // MARK: - Private
+
+ func showLoadingIndicator() {
+ loadingIndicator = indicatorPresenter?.present(.loading(label: VectorL10n.loading, isInteractionBlocking: true))
+ }
+
+ func hideLoadingIndicator() {
+ loadingIndicator = nil
+ }
+}
+
+// MARK: - PHPickerViewControllerDelegate
+@available(iOS 14, *)
+extension MediaPickerPresenter: PHPickerViewControllerDelegate {
+ func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
+ // TODO: Handle videos and multi-selection
+ guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else {
+ self.delegate?.mediaPickerPresenterDidCancel(self)
+ return
+ }
+
+ showLoadingIndicator()
+
+ provider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
+ guard let self = self else { return }
+
+ guard let image = image as? UIImage else {
+ DispatchQueue.main.async {
+ self.hideLoadingIndicator()
+ self.delegate?.mediaPickerPresenterDidCancel(self)
+ }
+ return
+ }
+
+ DispatchQueue.main.async {
+ self.hideLoadingIndicator()
+ self.delegate?.mediaPickerPresenter(self, didPickImage: image)
+ }
+ }
+ }
+}
diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift
index dd2a379ba..b9c99ad94 100644
--- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift
+++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift
@@ -65,6 +65,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
private var authenticationType: MXKAuthenticationType?
private var session: MXSession?
+ private var shouldShowDisplayNameScreen = false
+ private var shouldShowAvatarScreen = false
+
/// Whether all of the onboarding steps have been completed or not. `false` if there are more screens to be shown.
private var onboardingFinished = false
/// Whether authentication is complete. `true` once authenticated, verified and the app is ready to be shown.
@@ -182,6 +185,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
}
/// Displays the next view in the flow after the use case screen.
+ @available(iOS 14.0, *)
private func useCaseSelectionCoordinator(_ coordinator: OnboardingUseCaseSelectionCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) {
useCaseResult = result
showAuthenticationScreen()
@@ -247,12 +251,13 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
self.session = session
self.authenticationType = authenticationType
- // May need to move the spinner and key verification up to here in order to coordinate properly.
-
// Check whether another screen should be shown.
if #available(iOS 14.0, *) {
- if authenticationType == .register, let userId = session.credentials.userId, BuildSettings.onboardingShowAccountPersonalisation {
- showCongratulationsScreen(userId: userId)
+ if authenticationType == .register,
+ let userId = session.credentials.userId,
+ let userSession = UserSessionsService.shared.userSession(withUserId: userId),
+ BuildSettings.onboardingShowAccountPersonalization {
+ checkHomeserverCapabilities(for: userSession)
return
} else if Analytics.shared.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(for: session)
@@ -265,6 +270,24 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
completeIfReady()
}
+ /// Checks the capabilities of the user's homeserver in order to determine
+ /// whether or not the display name and avatar can be updated.
+ ///
+ /// Once complete this method will start the post authentication flow automatically.
+ @available(iOS 14.0, *)
+ private func checkHomeserverCapabilities(for userSession: UserSession) {
+ userSession.matrixSession.matrixRestClient.capabilities { [weak self] capabilities in
+ guard let self = self else { return }
+ self.shouldShowDisplayNameScreen = capabilities?.setDisplayName?.isEnabled == true
+ self.shouldShowAvatarScreen = capabilities?.setAvatarUrl?.isEnabled == true
+
+ self.beginPostAuthentication(for: userSession)
+ } failure: { [weak self] _ in
+ MXLog.warning("[OnboardingCoordinator] Homeserver capabilities not returned. Skipping personalisation")
+ self?.beginPostAuthentication(for: userSession)
+ }
+ }
+
/// Displays the next view in the flow after the authentication screen.
private func authenticationCoordinatorDidComplete(_ coordinator: AuthenticationCoordinatorProtocol) {
isShowingAuthentication = false
@@ -287,11 +310,19 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
// MARK: - Post-Authentication
+ /// Starts the part of the flow that comes after authentication for new users.
@available(iOS 14.0, *)
- private func showCongratulationsScreen(userId: String) {
+ private func beginPostAuthentication(for userSession: UserSession) {
+ showCongratulationsScreen(for: userSession)
+ }
+
+ /// Show the congratulations screen for new users. The screen will be configured based on the homeserver's capabilities.
+ @available(iOS 14.0, *)
+ private func showCongratulationsScreen(for userSession: UserSession) {
MXLog.debug("[OnboardingCoordinator] showCongratulationsScreen")
- let parameters = OnboardingCongratulationsCoordinatorParameters(userId: userId)
+ let parameters = OnboardingCongratulationsCoordinatorParameters(userSession: userSession,
+ personalizationDisabled: !shouldShowDisplayNameScreen && !shouldShowAvatarScreen)
let coordinator = OnboardingCongratulationsCoordinator(parameters: parameters)
coordinator.completion = { [weak self, weak coordinator] result in
@@ -308,21 +339,25 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
}
}
+ /// Displays the next view in the flow after the congratulations screen.
@available(iOS 14.0, *)
- private func congratulationsCoordinator(_ coordinator: OnboardingCongratulationsCoordinator, didCompleteWith result: OnboardingCongratulationsViewModelResult) {
- if let session = session {
- switch result {
- case .personaliseProfile:
- // TODO: Profile screens here instead.
- if Analytics.shared.shouldShowAnalyticsPrompt {
- showAnalyticsPrompt(for: session)
- return
- }
- case .takeMeHome:
- if Analytics.shared.shouldShowAnalyticsPrompt {
- showAnalyticsPrompt(for: session)
- return
- }
+ private func congratulationsCoordinator(_ coordinator: OnboardingCongratulationsCoordinator, didCompleteWith result: OnboardingCongratulationsCoordinatorResult) {
+ switch result {
+ case .personalizeProfile(let userSession):
+ if shouldShowDisplayNameScreen {
+ showDisplayNameScreen(for: userSession)
+ return
+ } else if shouldShowAvatarScreen {
+ showAvatarScreen(for: userSession)
+ return
+ } else if Analytics.shared.shouldShowAnalyticsPrompt {
+ showAnalyticsPrompt(for: userSession.matrixSession)
+ return
+ }
+ case .takeMeHome(let userSession):
+ if Analytics.shared.shouldShowAnalyticsPrompt {
+ showAnalyticsPrompt(for: userSession.matrixSession)
+ return
}
}
@@ -330,6 +365,84 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
completeIfReady()
}
+ /// Show the display name personalization screen for new users using the supplied user session.
+ @available(iOS 14.0, *)
+ private func showDisplayNameScreen(for userSession: UserSession) {
+ MXLog.debug("[OnboardingCoordinator]: showDisplayNameScreen")
+
+ let parameters = OnboardingDisplayNameCoordinatorParameters(userSession: userSession)
+ let coordinator = OnboardingDisplayNameCoordinator(parameters: parameters)
+
+ coordinator.completion = { [weak self, weak coordinator] session in
+ guard let self = self, let coordinator = coordinator else { return }
+ self.displayNameCoordinator(coordinator, didCompleteWith: session)
+ }
+
+ add(childCoordinator: coordinator)
+ coordinator.start()
+
+ navigationRouter.setRootModule(coordinator, hideNavigationBar: false, animated: true) { [weak self] in
+ self?.remove(childCoordinator: coordinator)
+ }
+ }
+
+ /// Displays the next view in the flow after the display name screen.
+ @available(iOS 14.0, *)
+ private func displayNameCoordinator(_ coordinator: OnboardingDisplayNameCoordinator, didCompleteWith userSession: UserSession) {
+ if shouldShowAvatarScreen {
+ showAvatarScreen(for: userSession)
+ return
+ } else if Analytics.shared.shouldShowAnalyticsPrompt {
+ showAnalyticsPrompt(for: userSession.matrixSession)
+ return
+ }
+
+ onboardingFinished = true
+ completeIfReady()
+ }
+
+ /// Show the avatar personalization screen for new users using the supplied user session.
+ @available(iOS 14.0, *)
+ private func showAvatarScreen(for userSession: UserSession) {
+ MXLog.debug("[OnboardingCoordinator]: showAvatarScreen")
+
+ let parameters = OnboardingAvatarCoordinatorParameters(userSession: userSession)
+ let coordinator = OnboardingAvatarCoordinator(parameters: parameters)
+
+ coordinator.completion = { [weak self, weak coordinator] session in
+ guard let self = self, let coordinator = coordinator else { return }
+ self.avatarCoordinator(coordinator, didCompleteWith: session)
+ }
+
+ add(childCoordinator: coordinator)
+ coordinator.start()
+
+ if navigationRouter.modules.isEmpty || !shouldShowDisplayNameScreen {
+ navigationRouter.setRootModule(coordinator, hideNavigationBar: false, animated: true) { [weak self] in
+ self?.remove(childCoordinator: coordinator)
+ }
+ } else {
+ navigationRouter.push(coordinator, animated: true) { [weak self] in
+ self?.remove(childCoordinator: coordinator)
+ }
+ }
+ }
+
+ /// Displays the next view in the flow after the avatar screen.
+ @available(iOS 14.0, *)
+ private func avatarCoordinator(_ coordinator: OnboardingAvatarCoordinator, didCompleteWith userSession: UserSession) {
+ if Analytics.shared.shouldShowAnalyticsPrompt {
+ showAnalyticsPrompt(for: userSession.matrixSession)
+ return
+ }
+
+ onboardingFinished = true
+ completeIfReady()
+ }
+
+ /// Shows the analytics prompt for the supplied session.
+ ///
+ /// Check `Analytics.shared.shouldShowAnalyticsPrompt` before calling this method.
@available(iOS 14.0, *)
private func showAnalyticsPrompt(for session: MXSession) {
MXLog.debug("[OnboardingCoordinator]: Invite the user to send analytics")
@@ -351,6 +464,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
}
}
+ /// Displays the next view in the flow after the analytics screen.
private func analyticsPromptCoordinatorDidComplete(_ coordinator: AnalyticsPromptCoordinator) {
onboardingFinished = true
completeIfReady()
@@ -358,6 +472,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
// MARK: - Finished
+ /// Calls the coordinator's completion handler if both `onboardingFinished` and `authenticationFinished`
+ /// are true. Otherwise displays any pending screens and waits to be called again.
private func completeIfReady() {
guard onboardingFinished else {
MXLog.debug("[OnboardingCoordinator] Delaying onboarding completion until all screens have been shown.")
diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m
index 0411cc9f1..0f8152c5e 100644
--- a/Riot/Modules/People/PeopleViewController.m
+++ b/Riot/Modules/People/PeopleViewController.m
@@ -68,8 +68,9 @@
// This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods).
self.recentsTableView.tag = RecentsDataSourceModePeople;
+ UIImage *fabImage = self.dataSource.currentSpace == nil ? AssetImages.peopleFloatingAction.image : AssetImages.addMemberFloatingAction.image;
// Add the (+) button programmatically
- plusButtonImageView = [self vc_addFABWithImage:AssetImages.peopleFloatingAction.image
+ plusButtonImageView = [self vc_addFABWithImage:fabImage
target:self
action:@selector(onPlusButtonPressed)];
}
@@ -104,9 +105,13 @@
[self.tableViewPaginationThrottler throttle:^{
NSInteger section = indexPath.section;
+ if (tableView.numberOfSections <= section)
+ {
+ return;
+ }
+
NSInteger numberOfRowsInSection = [tableView numberOfRowsInSection:section];
- if (tableView.numberOfSections > section
- && indexPath.row == numberOfRowsInSection - 1)
+ if (indexPath.row == numberOfRowsInSection - 1)
{
[self->recentsDataSource paginateInSection:section];
}
diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m
index 2b4569fba..5e91b0d51 100644
--- a/Riot/Modules/Room/DataSources/RoomDataSource.m
+++ b/Riot/Modules/Room/DataSources/RoomDataSource.m
@@ -968,11 +968,6 @@ const CGFloat kTypingCellHeight = 24;
- (void)threadingService:(MXThreadingService *)service didCreateNewThread:(MXThread *)thread direction:(MXTimelineDirection)direction
{
- if (self.threadId)
- {
- // no need to reload the thread screen
- return;
- }
if (direction == MXTimelineDirectionBackwards)
{
// no need to reload when paginating back
diff --git a/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.swift b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.swift
new file mode 100644
index 000000000..b6ad5e1c5
--- /dev/null
+++ b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.swift
@@ -0,0 +1,96 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// 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 Foundation
+import Reusable
+import UIKit
+
+@objcMembers
+final class LiveLocationSharingBannerView: UIView, NibLoadable, Themable {
+
+ // MARK: - Properties
+
+ // MARK: Outlets
+
+ @IBOutlet private weak var iconImageView: UIImageView!
+ @IBOutlet private weak var titleLabel: UILabel!
+ @IBOutlet private weak var stopButton: UIButton!
+
+ // MARK: Private
+
+ private var theme: Theme!
+
+ // MARK: Public
+
+ var didTapBackground: (() -> Void)?
+ var didTapStopButton: (() -> Void)?
+
+ // MARK: - Setup
+
+ static func instantiate() -> LiveLocationSharingBannerView {
+ let view = LiveLocationSharingBannerView.loadFromNib()
+ view.update(theme: ThemeService.shared().theme)
+ return view
+ }
+
+ // MARK: - Life cycle
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ self.setupBackgroundTapGestureRecognizer()
+
+ self.titleLabel.text = VectorL10n.liveLocationSharingBannerTitle
+ self.stopButton.setTitle(VectorL10n.liveLocationSharingBannerStop, for: .normal)
+ }
+
+ // MARK: - Public
+
+ func update(theme: Theme) {
+ self.theme = theme
+
+ let tintColor = theme.colors.background
+
+ self.backgroundColor = theme.tintColor
+
+ self.iconImageView.tintColor = tintColor
+
+ self.titleLabel.textColor = tintColor
+ self.titleLabel.font = theme.fonts.footnote
+
+ self.stopButton.vc_setTitleFont(theme.fonts.footnote)
+ self.stopButton.tintColor = tintColor
+ self.stopButton.setTitleColor(tintColor, for: .normal)
+ self.stopButton.setTitleColor(tintColor.withAlphaComponent(0.5), for: .highlighted)
+ }
+
+ // MARK: - Private
+
+ private func setupBackgroundTapGestureRecognizer() {
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundViewTap(_:)))
+ self.addGestureRecognizer(tapGestureRecognizer)
+ }
+
+ // MARK: - Actions
+
+ @objc private func handleBackgroundViewTap(_ gestureRecognizer: UITapGestureRecognizer) {
+ self.didTapBackground?()
+ }
+
+ @IBAction private func stopButtonAction(_ sender: Any) {
+ self.didTapStopButton?()
+ }
+}
diff --git a/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.xib b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.xib
new file mode 100644
index 000000000..f6c67f287
--- /dev/null
+++ b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.xib
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
index 2648fc89c..50f0871d5 100644
--- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
+++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
@@ -460,6 +460,7 @@
- (void)showRoomWithId:(NSString*)roomId
{
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomMemberDetail;
[[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:self.mainSession];
}
diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.h b/Riot/Modules/Room/Members/RoomParticipantsViewController.h
index bfc1d02c3..ada3003f3 100644
--- a/Riot/Modules/Room/Members/RoomParticipantsViewController.h
+++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.h
@@ -16,10 +16,9 @@
#import "SegmentedViewController.h"
-#import "ContactsTableViewController.h"
-
@class Contact;
@class RoomParticipantsViewController;
+@class AnalyticsScreenTracker;
/**
`RoomParticipantsViewController` delegate.
@@ -42,7 +41,7 @@
'RoomParticipantsViewController' instance is used to edit members of the room defined by the property 'mxRoom'.
When this property is nil, the view controller is empty.
*/
-@interface RoomParticipantsViewController : MXKViewController
+@interface RoomParticipantsViewController : MXKViewController
{
@protected
/**
@@ -79,6 +78,11 @@
*/
@property (nonatomic) MXRoom *mxRoom;
+/**
+ The ID of the parent space. `nil` for home space
+ */
+@property (nonatomic) NSString *parentSpaceId;
+
/**
Enable mention option in member details view. NO by default
*/
@@ -86,6 +90,7 @@
@property (nonatomic) BOOL showCancelBarButtonItem;
@property (nonatomic) BOOL showParticipantCustomAccessoryView;
+@property (nonatomic) BOOL showInviteUserFab;
/**
The delegate for the view controller.
diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.m b/Riot/Modules/Room/Members/RoomParticipantsViewController.m
index e419fe019..6ee51ea9d 100644
--- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m
+++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m
@@ -29,7 +29,7 @@
#import "RageShakeManager.h"
-@interface RoomParticipantsViewController ()
+@interface RoomParticipantsViewController ()
{
// Search result
NSString *currentSearchText;
@@ -49,12 +49,13 @@
id roomDidFlushDataNotificationObserver;
RoomMemberDetailsViewController *memberDetailsViewController;
- ContactsTableViewController *contactsPickerViewController;
UIAlertController *currentAlert;
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
id kThemeServiceDidChangeThemeNotificationObserver;
+
+ RoomParticipantsInviteCoordinatorBridgePresenter *invitePresenter;
}
@end
@@ -85,6 +86,7 @@
self.enableBarTintColorStatusChange = NO;
self.rageShakeManager = [RageShakeManager sharedManager];
self.showParticipantCustomAccessoryView = YES;
+ self.showInviteUserFab = YES;
}
- (void)viewDidLoad
@@ -143,11 +145,13 @@
[self.tableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:@"ParticipantTableViewCellId"];
-
- // Add invite members button programmatically
- [self vc_addFABWithImage:AssetImages.addMemberFloatingAction.image
- target:self
- action:@selector(onAddParticipantButtonPressed)];
+ if (_showInviteUserFab)
+ {
+ // Add invite members button programmatically
+ [self vc_addFABWithImage:AssetImages.addMemberFloatingAction.image
+ target:self
+ action:@selector(onAddParticipantButtonPressed)];
+ }
// Observe user interface theme change.
kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
@@ -264,12 +268,6 @@
[memberDetailsViewController destroy];
memberDetailsViewController = nil;
}
-
- if (contactsPickerViewController)
- {
- [contactsPickerViewController destroy];
- contactsPickerViewController = nil;
- }
}
- (void)viewWillDisappear:(BOOL)animated
@@ -544,50 +542,9 @@
- (void)onAddParticipantButtonPressed
{
- // Push the contacts picker.
- contactsPickerViewController = [ContactsTableViewController contactsTableViewController];
-
- // Set delegate to handle action on member (start chat, mention)
- contactsPickerViewController.contactsTableViewControllerDelegate = self;
-
- // Prepare its data source
- ContactsDataSource *contactsDataSource = [[ContactsDataSource alloc] initWithMatrixSession:self.mxRoom.mxSession];
- contactsDataSource.areSectionsShrinkable = YES;
- contactsDataSource.displaySearchInputInContactsList = YES;
- contactsDataSource.forceMatrixIdInDisplayName = YES;
- // Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user.
- contactsDataSource.contactCellAccessoryImage = [AssetImages.plusIcon.image vc_tintedImageUsingColor:ThemeService.shared.theme.textPrimaryColor];
-
- // List all the participants matrix user id to ignore them during the contacts search.
- for (Contact *contact in actualParticipants)
- {
- contactsDataSource.ignoredContactsByMatrixId[contact.mxMember.userId] = contact;
- }
- for (Contact *contact in invitedParticipants)
- {
- if (contact.mxMember)
- {
- contactsDataSource.ignoredContactsByMatrixId[contact.mxMember.userId] = contact;
- }
- }
- if (userParticipant)
- {
- contactsDataSource.ignoredContactsByMatrixId[userParticipant.mxMember.userId] = userParticipant;
- }
-
- [contactsPickerViewController showSearch:YES];
- contactsPickerViewController.searchBar.placeholder = [VectorL10n roomParticipantsInviteAnotherUser];
-
- // Apply the search pattern if any
- if (currentSearchText)
- {
- contactsPickerViewController.searchBar.text = currentSearchText;
- [contactsDataSource searchWithPattern:currentSearchText forceReset:YES];
- }
-
- [contactsPickerViewController displayList:contactsDataSource];
-
- [self pushViewController:contactsPickerViewController];
+ self->invitePresenter = [[RoomParticipantsInviteCoordinatorBridgePresenter alloc] initWithSession:self.mxRoom.mxSession room:self.mxRoom parentSpaceId:self.parentSpaceId currentSearchText:currentSearchText actualParticipants:actualParticipants invitedParticipants:invitedParticipants userParticipant:userParticipant];
+ self->invitePresenter.delegate = self;
+ [self->invitePresenter presentFrom:self animated:true];
}
- (void)refreshParticipantsFromRoomMembers
@@ -1262,13 +1219,6 @@
}
}
-#pragma mark - ContactsTableViewControllerDelegate
-
-- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact
-{
- [self didSelectInvitableContact:contact];
-}
-
#pragma mark - Actions
- (void)onDeleteAt:(NSIndexPath*)path
@@ -1493,149 +1443,6 @@
[self withdrawViewControllerAnimated:YES completion:nil];
}
-#pragma mark -
-
-- (void)didSelectInvitableContact:(MXKContact*)contact
-{
- __weak typeof(self) weakSelf = self;
-
- if (currentAlert)
- {
- [currentAlert dismissViewControllerAnimated:NO completion:nil];
- currentAlert = nil;
- }
-
- // Invite ?
- NSString *promptMsg = [VectorL10n roomParticipantsInvitePromptMsg:contact.displayName];
- currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomParticipantsInvitePromptTitle]
- message:promptMsg
- preferredStyle:UIAlertControllerStyleAlert];
-
- [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
- style:UIAlertActionStyleCancel
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self->currentAlert = nil;
- }
-
- }]];
-
- [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n invite]
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self->currentAlert = nil;
-
- NSArray *identifiers = contact.matrixIdentifiers;
- NSString *participantId;
-
- if (identifiers.count)
- {
- participantId = identifiers.firstObject;
-
- // Invite this user if a room is defined
- [self addPendingActionMask];
- [self.mxRoom inviteUser:participantId success:^{
-
- __strong __typeof(weakSelf)self = weakSelf;
- [self removePendingActionMask];
-
- // Refresh display by removing the contacts picker
- [self->contactsPickerViewController withdrawViewControllerAnimated:YES completion:nil];
-
- } failure:^(NSError *error) {
-
- __strong __typeof(weakSelf)self = weakSelf;
- [self removePendingActionMask];
-
- MXLogDebug(@"[RoomParticipantsVC] Invite %@ failed", participantId);
- // Alert user
- [[AppDelegate theDelegate] showErrorAsAlert:error];
- }];
- }
- else
- {
- if (contact.emailAddresses.count)
- {
- // This is a local contact, consider the first email by default.
- // TODO: Prompt the user to select the right email.
- MXKEmail *email = contact.emailAddresses.firstObject;
- participantId = email.emailAddress;
- }
- else
- {
- // This is the text filled by the user.
- participantId = contact.displayName;
- }
-
- // Is it an email or a Matrix user ID?
- if ([MXTools isEmailAddress:participantId])
- {
- [self addPendingActionMask];
- [self.mxRoom inviteUserByEmail:participantId success:^{
-
- __strong __typeof(weakSelf)self = weakSelf;
- [self removePendingActionMask];
-
- // Refresh display by removing the contacts picker
- [self->contactsPickerViewController withdrawViewControllerAnimated:YES completion:nil];
-
- } failure:^(NSError *error) {
-
- __strong __typeof(weakSelf)self = weakSelf;
- [self removePendingActionMask];
-
- MXLogDebug(@"[RoomParticipantsVC] Invite be email %@ failed", participantId);
-
- // Alert user
- if ([error.domain isEqualToString:kMXRestClientErrorDomain]
- && error.code == MXRestClientErrorMissingIdentityServer)
- {
- NSString *message = [VectorL10n errorInvite3pidWithNoIdentityServer];
- [[AppDelegate theDelegate] showAlertWithTitle:message message:nil];
- }
- else
- {
- [[AppDelegate theDelegate] showErrorAsAlert:error];
- }
- }];
- }
- else //if ([MXTools isMatrixUserIdentifier:participantId])
- {
- [self addPendingActionMask];
- [self.mxRoom inviteUser:participantId success:^{
-
- __strong __typeof(weakSelf)self = weakSelf;
- [self removePendingActionMask];
-
- // Refresh display by removing the contacts picker
- [self->contactsPickerViewController withdrawViewControllerAnimated:YES completion:nil];
-
- } failure:^(NSError *error) {
-
- __strong __typeof(weakSelf)self = weakSelf;
- [self removePendingActionMask];
-
- MXLogDebug(@"[RoomParticipantsVC] Invite %@ failed", participantId);
- // Alert user
- [[AppDelegate theDelegate] showErrorAsAlert:error];
- }];
- }
- }
- }
-
- }]];
-
- [currentAlert mxk_setAccessibilityIdentifier:@"RoomParticipantsVCInviteAlert"];
- [self presentViewController:currentAlert animated:YES completion:nil];
-}
-
#pragma mark - UISearchBar delegate
- (void)refreshSearchBarItemsColor:(UISearchBar *)searchBar
@@ -1765,4 +1572,21 @@
[searchBar resignFirstResponder];
}
+#pragma mark - RoomParticipantsInviteCoordinatorBridgePresenterDelegate
+
+- (void)roomParticipantsInviteCoordinatorBridgePresenterDidComplete:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ self->invitePresenter = nil;
+}
+
+- (void)roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ [self addPendingActionMask];
+}
+
+- (void)roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ [self removePendingActionMask];
+}
+
@end
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerCoordinator.swift b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerCoordinator.swift
new file mode 100644
index 000000000..97f39d255
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerCoordinator.swift
@@ -0,0 +1,141 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+
+class ContactsPickerCoordinator: ContactsPickerCoordinatorProtocol {
+
+ private weak var currentAlert: UIAlertController?
+
+ // MARK: - Private
+
+ private let session: MXSession?
+ private let room: MXRoom?
+ private let initialSearchText: String?
+ private var actualParticipants: [Contact]?
+ private var invitedParticipants: [Contact]?
+ private var userParticipant: Contact?
+
+ private let navigationRouter: NavigationRouterType
+ private weak var contactsPickerViewController: ContactsTableViewController?
+ private var viewModel: ContactsPickerViewModelProtocol?
+
+ // MARK: Public
+
+ internal var childCoordinators: [Coordinator] = []
+ weak var delegate: ContactsPickerCoordinatorDelegate?
+
+ // MARK: - Setup
+
+ init(session: MXSession, room: MXRoom, initialSearchText: String?, actualParticipants: [Contact]?, invitedParticipants: [Contact]?, userParticipant: Contact?, navigationRouter: NavigationRouterType? = nil) {
+ self.session = session
+ self.room = room
+ self.initialSearchText = initialSearchText
+
+ self.actualParticipants = actualParticipants
+ self.invitedParticipants = invitedParticipants
+ self.userParticipant = userParticipant
+
+ if let navigationRouter = navigationRouter {
+ self.navigationRouter = navigationRouter
+ } else {
+ self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
+ }
+ }
+
+ // MARK: - Public methods
+
+ func start() {
+ guard let room = self.room else {
+ MXLog.error("[ContactsCoordinator] start: no room")
+ return
+ }
+
+ let viewModel = ContactsPickerViewModel(room: room, actualParticipants: self.actualParticipants, invitedParticipants: self.invitedParticipants, userParticipant: self.userParticipant)
+ viewModel.coordinatorDelegate = self
+ self.viewModel = viewModel
+
+ guard viewModel.areParticipantsLoaded else {
+ viewModel.loadParticipants()
+ return
+ }
+
+ startWithParticipants()
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.navigationRouter.toPresentable()
+ }
+
+ // MARK: - Private methods
+
+ private func startWithParticipants() {
+ // Push the contacts picker.
+ let contactsViewController = RoomInviteViewController()
+ viewModel?.prepare(contactsViewController: contactsViewController, currentSearchText: initialSearchText)
+ self.navigationRouter.push(contactsViewController, animated: true) { [weak self] in
+ guard let self = self else { return }
+ self.delegate?.contactsPickerCoordinatorDidClose(self)
+ }
+ contactsPickerViewController = contactsViewController
+ }
+}
+
+// MARK: - ContactsViewModelCoordinatorDelegate
+
+extension ContactsPickerCoordinator: ContactsPickerViewModelCoordinatorDelegate {
+ func contactsPickerViewModelDidStartLoading(_ viewModel: ContactsPickerViewModelProtocol) {
+ delegate?.contactsPickerCoordinatorDidStartLoading(self)
+ }
+
+ func contactsPickerViewModelDidEndLoading(_ viewModel: ContactsPickerViewModelProtocol) {
+ delegate?.contactsPickerCoordinatorDidEndLoading(self)
+ startWithParticipants()
+ }
+
+ func contactsPickerViewModelDidStartInvite(_ viewModel: ContactsPickerViewModelProtocol) {
+ contactsPickerViewController?.startActivityIndicator()
+ }
+
+ func contactsPickerViewModelDidEndInvite(_ viewModel: ContactsPickerViewModelProtocol) {
+ contactsPickerViewController?.stopActivityIndicator()
+ contactsPickerViewController?.withdrawViewController(animated: true, completion: {
+ self.delegate?.contactsPickerCoordinatorDidClose(self)
+ })
+ }
+
+ func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, inviteFailedWithError error: Error?) {
+ contactsPickerViewController?.stopActivityIndicator()
+ if let error = error {
+ AppDelegate.theDelegate().showError(asAlert: error)
+ }
+ }
+
+ func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, display message: String, title: String, actions: [UIAlertAction]) {
+ currentAlert?.dismiss(animated: false, completion: nil)
+ currentAlert = nil
+
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ for action in actions {
+ alert.addAction(action)
+ }
+
+ alert.mxk_setAccessibilityIdentifier("RoomParticipantsVCInviteAlert")
+ navigationRouter.present(alert, animated: true)
+
+ currentAlert = alert
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerCoordinatorProtocol.swift b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerCoordinatorProtocol.swift
new file mode 100644
index 000000000..a848fc4d4
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerCoordinatorProtocol.swift
@@ -0,0 +1,27 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+
+protocol ContactsPickerCoordinatorDelegate: AnyObject {
+ func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol)
+ func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol)
+ func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol)
+}
+
+protocol ContactsPickerCoordinatorProtocol: Coordinator, Presentable {
+ var delegate: ContactsPickerCoordinatorDelegate? { get }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift
new file mode 100644
index 000000000..d4939f94c
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift
@@ -0,0 +1,283 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+
+class ContactsPickerViewModel: NSObject, ContactsPickerViewModelProtocol {
+
+ private class RoomMembers {
+ var actualParticipants: [Contact] = []
+ var invitedParticipants: [Contact] = []
+ var userParticipant: Contact?
+ }
+
+ // MARK: - Properties
+
+ weak var coordinatorDelegate: ContactsPickerViewModelCoordinatorDelegate?
+ private(set) var areParticipantsLoaded: Bool = false
+
+ // MARK: - Private
+
+ private let room: MXRoom
+ private var actualParticipants: [Contact]?
+ private var invitedParticipants: [Contact]?
+ private var userParticipant: Contact?
+
+ // MARK: - Setup
+
+ init(room: MXRoom, actualParticipants: [Contact]?, invitedParticipants: [Contact]?, userParticipant: Contact?) {
+ self.room = room
+ self.actualParticipants = actualParticipants
+ self.invitedParticipants = invitedParticipants
+ self.userParticipant = userParticipant
+
+ areParticipantsLoaded = actualParticipants != nil && invitedParticipants != nil && userParticipant != nil
+
+ super.init()
+ }
+
+ // MARK: - Public
+
+ func loadParticipants() {
+ coordinatorDelegate?.contactsPickerViewModelDidStartLoading(self)
+
+ let roomMembers = RoomMembers()
+
+ // Retrieve the current members from the room state
+ room.state { [weak self] roomState in
+ guard let self = self else {
+ return
+ }
+
+ guard let roomState = roomState, let members = roomState.members.membersWithoutConferenceUser(), let session = self.room.mxSession, let myUserId = session.myUserId, let roomThirdPartyInvites = roomState.thirdPartyInvites else {
+ self.finalize(participants: roomMembers)
+ return
+ }
+
+ for member in members {
+ if member.userId == myUserId {
+ if member.membership == .join || member.membership == .invite {
+ let displayName = VectorL10n.you
+ if let participant = Contact(matrixContactWithDisplayName: displayName, andMatrixID: myUserId) {
+ participant.mxMember = roomState.members.member(withUserId: myUserId)
+ roomMembers.userParticipant = participant
+ }
+ }
+ } else {
+ self.handle(roomMember: member, session: session, members: roomMembers)
+ }
+ }
+
+ for invite in roomThirdPartyInvites {
+ self.add(thirdPartyParticipant: invite, roomState: roomState, members: roomMembers)
+ }
+
+ self.finalize(participants: roomMembers)
+ }
+ }
+
+ func prepare(contactsViewController: RoomInviteViewController, currentSearchText: String?) -> Bool {
+ contactsViewController.room = self.room
+
+ // Set delegate to handle action on member (start chat, mention)
+ contactsViewController.contactsTableViewControllerDelegate = self
+
+ // Prepare its data source
+ guard let contactsDataSource = ContactsDataSource(matrixSession: room.mxSession) else {
+ MXLog.error("[ContactsPickerViewModel] prepare: failed to instantiate ContactsDataSource")
+ return false
+ }
+ contactsDataSource.areSectionsShrinkable = true
+ contactsDataSource.displaySearchInputInContactsList = true
+ contactsDataSource.forceMatrixIdInDisplayName = true
+
+ // Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user.
+ contactsDataSource.contactCellAccessoryImage = Asset.Images.plusIcon.image.vc_tintedImage(usingColor: ThemeService.shared().theme.textPrimaryColor)
+
+ // List all the participants matrix user id to ignore them during the contacts search.
+ for contact in actualParticipants ?? [] {
+ if let userId = contact.mxMember.userId {
+ contactsDataSource.ignoredContactsByMatrixId[userId] = contact
+ }
+ }
+
+ for contact in invitedParticipants ?? [] {
+ if let userId = contact.mxMember?.userId {
+ contactsDataSource.ignoredContactsByMatrixId[userId] = contact
+ }
+ }
+
+ if let userParticipantId = self.userParticipant?.mxMember.userId {
+ contactsDataSource.ignoredContactsByMatrixId[userParticipantId] = userParticipant
+ }
+
+ contactsViewController.showSearch(true)
+ contactsViewController.searchBar.placeholder = VectorL10n.roomParticipantsInviteAnotherUser
+
+ // Apply the search pattern if any
+ if currentSearchText != nil {
+ contactsViewController.searchBar.text = currentSearchText
+ contactsDataSource.search(withPattern: currentSearchText, forceReset: true)
+ }
+
+ contactsViewController.displayList(contactsDataSource)
+
+ return true
+ }
+
+ // MARK: - Private
+
+ private func handle(roomMember: MXRoomMember, session: MXSession, members: RoomMembers) {
+ // Add this member after checking his status
+ guard roomMember.membership == .join || roomMember.membership == .invite else {
+ return
+ }
+
+ // Prepare the display name of this member
+ var displayName = roomMember.displayname
+ if displayName.isEmptyOrNil {
+ // Look for the corresponding MXUser in matrix session
+ if let user = session.user(withUserId: roomMember.userId) {
+ displayName = user.displayname.isEmptyOrNil ? user.userId : user.displayname
+ } else {
+ displayName = roomMember.userId
+ }
+ }
+
+ // Create the contact related to this member
+ if let contact = Contact(matrixContactWithDisplayName: displayName, andMatrixID: roomMember.userId) {
+ contact.mxMember = roomMember
+
+ if roomMember.membership == .invite {
+ members.invitedParticipants.append(contact)
+ } else {
+ members.actualParticipants.append(contact)
+ }
+ }
+ }
+
+ private func add(thirdPartyParticipant invite: MXRoomThirdPartyInvite, roomState: MXRoomState, members: RoomMembers) {
+ // If the homeserver has converted the 3pid invite into a room member, do no show it
+ // If the invite has been revoked (null display name), do not show it too.
+ guard let displayName = invite.displayname, roomState.member(withThirdPartyInviteToken: invite.token) == nil else {
+ return
+ }
+
+ if let contact = Contact(matrixContactWithDisplayName: displayName, andMatrixID: nil) {
+ contact.isThirdPartyInvite = true
+ contact.mxThirdPartyInvite = invite
+ members.invitedParticipants.append(contact)
+ }
+ }
+
+ private func finalize(participants roomMembers: RoomMembers) {
+ self.actualParticipants = roomMembers.actualParticipants
+ self.invitedParticipants = roomMembers.invitedParticipants
+ self.userParticipant = roomMembers.userParticipant
+ self.coordinatorDelegate?.contactsPickerViewModelDidEndLoading(self)
+ }
+}
+
+// MARK: - ContactsTableViewControllerDelegate
+extension ContactsPickerViewModel: ContactsTableViewControllerDelegate {
+
+ func contactsTableViewController(_ contactsTableViewController: ContactsTableViewController!, didSelect contact: MXKContact?) {
+ guard let contact = contact else {
+ MXLog.error("[ContactsPickerViewModel] contactsTableViewController: nil contact found")
+ return
+ }
+
+ let roomName = room.displayName ?? VectorL10n.spaceTag
+ let message = VectorL10n.roomParticipantsInvitePromptToMsg(contact.displayName, roomName)
+
+ coordinatorDelegate?.contactsPickerViewModel(self, display: message, title: VectorL10n.roomParticipantsInvitePromptTitle, actions: [
+ UIAlertAction(title: VectorL10n.cancel, style: .cancel, handler: nil),
+ UIAlertAction(title: VectorL10n.invite, style: .default, handler: { [weak self] action in
+ self?.invite(contact: contact)
+ })
+ ])
+ }
+
+ private func invite(contact: MXKContact) {
+ if let identifiers = contact.matrixIdentifiers as? [String], let participantId = identifiers.first {
+
+ // Invite this user if a room is defined
+ self.coordinatorDelegate?.contactsPickerViewModelDidStartInvite(self)
+ room.invite(.userId(participantId)) { [weak self] response in
+ guard let self = self else { return }
+
+ switch response {
+ case .success:
+ self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
+ case .failure:
+ MXLog.error("[ContactsPickerViewModel] Failed to invite \(participantId) due to error; \(response.error ?? "nil")")
+ self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
+ }
+ }
+ } else {
+ let _participantId: String?
+
+ if let emailAddresses = contact.emailAddresses as? [MXKEmail], let email = emailAddresses.first {
+ // This is a local contact, consider the first email by default.
+ // TODO: Prompt the user to select the right email.
+ _participantId = email.emailAddress
+ } else {
+ // This is the text filled by the user.
+ _participantId = contact.displayName
+ }
+
+ guard let participantId = _participantId else {
+ MXLog.error("[ContactsPickerViewModel] invite: unexpectedly found participantId nil")
+ return
+ }
+
+ self.coordinatorDelegate?.contactsPickerViewModelDidStartInvite(self)
+ // Is it an email or a Matrix user ID?
+ if MXTools.isEmailAddress(participantId) {
+ room.invite(.email(participantId)) { [weak self] response in
+ guard let self = self else { return }
+
+ switch response {
+ case .success:
+ self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
+ case .failure:
+ MXLog.error("[ContactsPickerViewModel] Failed to invite \(participantId) by email due to error; \(response.error ?? "nil")")
+
+ if let error = response.error as NSError?, error.domain == kMXRestClientErrorDomain, error.code == MXRestClientErrorMissingIdentityServer {
+ self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: nil)
+ AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.errorInvite3pidWithNoIdentityServer, message: nil)
+ } else {
+ self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
+ }
+ }
+ }
+ } else {
+ room.invite(.userId(participantId)) { [weak self] response in
+ guard let self = self else { return }
+
+ switch response {
+ case .success:
+ self.coordinatorDelegate?.contactsPickerViewModelDidEndInvite(self)
+ case .failure:
+ MXLog.error("[ContactsPickerViewModel] Failed to invite \(participantId) due to error; \(response.error ?? "nil")")
+ self.coordinatorDelegate?.contactsPickerViewModel(self, inviteFailedWithError: response.error)
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModelProtocol.swift b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModelProtocol.swift
new file mode 100644
index 000000000..3ffb6698a
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModelProtocol.swift
@@ -0,0 +1,34 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+
+protocol ContactsPickerViewModelCoordinatorDelegate: AnyObject {
+ func contactsPickerViewModelDidStartLoading(_ viewModel: ContactsPickerViewModelProtocol)
+ func contactsPickerViewModelDidEndLoading(_ viewModel: ContactsPickerViewModelProtocol)
+ func contactsPickerViewModelDidStartInvite(_ viewModel: ContactsPickerViewModelProtocol)
+ func contactsPickerViewModelDidEndInvite(_ viewModel: ContactsPickerViewModelProtocol)
+ func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, inviteFailedWithError error: Error?)
+ func contactsPickerViewModel(_ viewModel: ContactsPickerViewModelProtocol, display message: String, title: String, actions: [UIAlertAction])
+}
+
+protocol ContactsPickerViewModelProtocol {
+ var coordinatorDelegate: ContactsPickerViewModelCoordinatorDelegate? { get set }
+ var areParticipantsLoaded: Bool { get }
+
+ func loadParticipants()
+ @discardableResult func prepare(contactsViewController: RoomInviteViewController, currentSearchText: String?) -> Bool
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/RoomInviteViewController.swift b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/RoomInviteViewController.swift
new file mode 100644
index 000000000..900bd0c3c
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/RoomInviteViewController.swift
@@ -0,0 +1,57 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+
+class RoomInviteViewController: ContactsTableViewController {
+
+ var room: MXRoom?
+ var roomAlias: String?
+
+ private lazy var shareLinkPresenter: ShareInviteLinkPresenter = ShareInviteLinkPresenter()
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ roomAlias = room?.summary?.aliases?.first
+ setupShareInviteLinkHeader()
+ }
+
+ private func setupShareInviteLinkHeader() {
+ guard roomAlias != nil, RiotSettings.shared.allowInviteExernalUsers else {
+ contactsTableView.tableHeaderView = nil
+ return
+ }
+
+ let inviteHeaderView = ShareInviteLinkHeaderView.instantiate()
+ inviteHeaderView.delegate = self
+ contactsTableView.tableHeaderView = inviteHeaderView
+ }
+
+ private func showInviteLink(from sourceView: UIView?) {
+ guard let room = room else {
+ return
+ }
+ shareLinkPresenter.present(for: room, from: self, sourceView: sourceView, animated: true)
+ }
+}
+
+// MARK: - ShareInviteLinkHeaderViewDelegate
+extension RoomInviteViewController: ShareInviteLinkHeaderViewDelegate {
+ func shareInviteLinkHeaderView(_ headerView: ShareInviteLinkHeaderView, didTapButton button: UIButton) {
+ showInviteLink(from: button)
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinator.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinator.swift
new file mode 100644
index 000000000..d8ae2d9b9
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinator.swift
@@ -0,0 +1,82 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 Foundation
+import UIKit
+
+final class OptionListCoordinator: OptionListCoordinatorProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: OptionListCoordinatorParameters
+ private var optionListViewModel: OptionListViewModelProtocol
+ private let optionListViewController: OptionListViewController
+ private lazy var slidingModalPresenter: SlidingModalPresenter = SlidingModalPresenter()
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+
+ weak var delegate: OptionListCoordinatorDelegate?
+
+ // MARK: - Setup
+
+ init(parameters: OptionListCoordinatorParameters) {
+ self.parameters = parameters
+ let optionListViewModel = OptionListViewModel(title: self.parameters.title, options: self.parameters.options)
+ let optionListViewController = OptionListViewController.instantiate(with: optionListViewModel)
+ self.optionListViewModel = optionListViewModel
+ self.optionListViewController = optionListViewController
+ }
+
+ // MARK: - Public
+
+ func start() {
+ self.optionListViewModel.coordinatorDelegate = self
+
+ if let rootViewController = self.parameters.navigationRouter?.toPresentable() {
+ slidingModalPresenter.present(optionListViewController, from: rootViewController, animated: true, completion: nil)
+ }
+ }
+
+ func dismiss(animated: Bool, completion: (() -> Void)?) {
+ slidingModalPresenter.dismiss(animated: animated, completion: completion)
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.optionListViewController
+ }
+}
+
+// MARK: - OptionListViewModelCoordinatorDelegate
+extension OptionListCoordinator: OptionListViewModelCoordinatorDelegate {
+ func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didSelectOptionAt index: Int) {
+ dismiss(animated: false) {
+ self.delegate?.optionListCoordinator(self, didSelectOptionAt: index)
+ }
+ }
+
+ func optionListViewModelDidCancel(_ viewModel: OptionListViewModelProtocol) {
+ dismiss(animated: true) {
+ self.delegate?.optionListCoordinatorDidCancel(self)
+ }
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinatorParameters.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinatorParameters.swift
new file mode 100644
index 000000000..ab6c8ee66
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinatorParameters.swift
@@ -0,0 +1,29 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 Foundation
+
+/// OptionListCoordinator input parameters
+struct OptionListCoordinatorParameters {
+
+ let title: String?
+ let options: [OptionListItemViewData]
+
+ let navigationRouter: NavigationRouterType?
+
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinatorProtocol.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinatorProtocol.swift
new file mode 100644
index 000000000..80176dbdb
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListCoordinatorProtocol.swift
@@ -0,0 +1,29 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 Foundation
+
+protocol OptionListCoordinatorDelegate: AnyObject {
+ func optionListCoordinator(_ coordinator: OptionListCoordinatorProtocol, didSelectOptionAt index: Int)
+ func optionListCoordinatorDidCancel(_ coordinator: OptionListCoordinatorProtocol)
+}
+
+/// `OptionListCoordinatorProtocol` is a protocol describing a Coordinator that handle invite options screen navigation flow.
+protocol OptionListCoordinatorProtocol: Coordinator, Presentable {
+ var delegate: OptionListCoordinatorDelegate? { get }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListItemViewData.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListItemViewData.swift
new file mode 100644
index 000000000..8ad0c0225
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListItemViewData.swift
@@ -0,0 +1,37 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 UIKit
+
+class OptionListItemViewData {
+ let title: String?
+ let detail: String?
+ let image: UIImage?
+ let accessoryImage: UIImage?
+ let enabled: Bool
+
+ init(title: String? = nil,
+ detail: String? = nil,
+ image: UIImage? = nil,
+ accessoryImage: UIImage? = Asset.Images.chevron.image,
+ enabled: Bool = true) {
+ self.title = title
+ self.detail = detail
+ self.image = image
+ self.accessoryImage = accessoryImage
+ self.enabled = enabled
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewAction.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewAction.swift
new file mode 100644
index 000000000..ec8f53c50
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewAction.swift
@@ -0,0 +1,26 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 Foundation
+
+/// OptionListViewController view actions exposed to view model
+enum OptionListViewAction {
+ case loadData
+ case selected(_ index: Int)
+ case cancel
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewCell.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewCell.swift
new file mode 100644
index 000000000..8e5a8d93f
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewCell.swift
@@ -0,0 +1,85 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+import Reusable
+
+class OptionListViewCell: UITableViewCell, NibReusable {
+
+ // MARK: - Properties
+
+ @IBOutlet private weak var iconView: UIImageView!
+ @IBOutlet private weak var titleLabel: UILabel!
+ @IBOutlet private weak var detailLabel: UILabel!
+ @IBOutlet private weak var selectionView: UIView!
+ @IBOutlet private weak var chevronView: UIImageView!
+
+ var isEnabled: Bool = true {
+ didSet {
+ self.contentView.alpha = isEnabled ? 1 : 0.3
+ }
+ }
+
+ // MARK: - Private
+
+ private var theme: Theme?
+
+ // MARK: - Life cycle
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ self.selectionStyle = .none
+ self.selectionView.layer.cornerRadius = 8.0
+ self.selectionView.layer.masksToBounds = true
+ }
+
+ override func setSelected(_ selected: Bool, animated: Bool) {
+ if isEnabled {
+ super.setSelected(selected, animated: animated)
+
+ UIView.animate(withDuration: animated ? 0.2 : 0.0) {
+ self.selectionView.transform = selected ? .init(scaleX: 0.95, y: 0.95) : .identity
+ }
+ }
+ }
+
+ // MARK: - Public
+
+ func update(with viewData: OptionListItemViewData) {
+ self.iconView.image = viewData.image?.withRenderingMode(.alwaysTemplate)
+ self.titleLabel.text = viewData.title
+ self.detailLabel.text = viewData.detail
+ self.chevronView.image = viewData.accessoryImage?.withRenderingMode(.alwaysTemplate)
+ self.isEnabled = viewData.enabled
+ }
+
+ func update(theme: Theme) {
+ self.theme = theme
+ self.backgroundColor = theme.colors.background
+ self.iconView.tintColor = theme.colors.secondaryContent
+
+ self.titleLabel.textColor = theme.colors.primaryContent
+ self.titleLabel.font = theme.fonts.bodySB
+
+ self.detailLabel.textColor = theme.colors.secondaryContent
+ self.detailLabel.font = theme.fonts.footnote
+
+ self.selectionView.backgroundColor = theme.colors.quinaryContent
+
+ self.chevronView.tintColor = theme.colors.quarterlyContent
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewCell.xib b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewCell.xib
new file mode 100644
index 000000000..a0071edab
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewCell.xib
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewController.storyboard b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewController.storyboard
new file mode 100644
index 000000000..54cf57320
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewController.storyboard
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewController.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewController.swift
new file mode 100644
index 000000000..77ce45a0a
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewController.swift
@@ -0,0 +1,214 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 UIKit
+
+final class OptionListViewController: UIViewController {
+
+ // MARK: - Constants
+
+ private enum Constants {
+ static let estimatedRowHeight: CGFloat = 80.0
+ }
+
+ // MARK: - Properties
+
+ // MARK: Outlets
+
+ @IBOutlet private weak var titleLabel: UILabel!
+ @IBOutlet private weak var closeButton: UIButton!
+ @IBOutlet private weak var tableView: UITableView!
+ @IBOutlet private weak var bottomMargin: NSLayoutConstraint!
+
+ // MARK: Private
+
+ private var viewModel: OptionListViewModelProtocol!
+ private var theme: Theme!
+ private var keyboardAvoider: KeyboardAvoider?
+ private var errorPresenter: MXKErrorPresentation!
+ private var activityPresenter: ActivityIndicatorPresenter!
+ private var options: [OptionListItemViewData] = []
+
+ // MARK: - Setup
+
+ class func instantiate(with viewModel: OptionListViewModelProtocol) -> OptionListViewController {
+ let viewController = StoryboardScene.OptionListViewController.initialScene.instantiate()
+ viewController.viewModel = viewModel
+ viewController.theme = ThemeService.shared().theme
+ return viewController
+ }
+
+ // MARK: - Life cycle
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Do any additional setup after loading the view.
+
+ self.setupViews()
+ self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.tableView)
+ self.activityPresenter = ActivityIndicatorPresenter()
+ self.errorPresenter = MXKErrorAlertPresentation()
+
+ self.registerThemeServiceDidChangeThemeNotification()
+ self.update(theme: self.theme)
+
+ self.viewModel.viewDelegate = self
+
+ self.viewModel.process(viewAction: .loadData)
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ self.keyboardAvoider?.startAvoiding()
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+
+ self.keyboardAvoider?.stopAvoiding()
+ }
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return self.theme.statusBarStyle
+ }
+
+ // MARK: - Private
+
+ private func update(theme: Theme) {
+ self.theme = theme
+
+ self.view.backgroundColor = theme.backgroundColor
+ self.tableView.backgroundColor = theme.backgroundColor
+
+ self.titleLabel.textColor = theme.colors.primaryContent
+ self.titleLabel.font = theme.fonts.title3SB
+
+ self.closeButton.backgroundColor = theme.roomInputTextBorder
+ self.closeButton.tintColor = theme.noticeSecondaryColor
+
+ self.tableView.reloadData()
+ }
+
+ private func registerThemeServiceDidChangeThemeNotification() {
+ NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
+ }
+
+ @objc private func themeDidChange() {
+ self.update(theme: ThemeService.shared().theme)
+ }
+
+ private func setupViews() {
+ self.setupTableView()
+
+ self.closeButton.layer.masksToBounds = true
+ self.closeButton.layer.cornerRadius = self.closeButton.bounds.height / 2
+ }
+
+ private func setupTableView() {
+ self.tableView.separatorStyle = .none
+ self.tableView.rowHeight = UITableView.automaticDimension
+ self.tableView.estimatedRowHeight = Constants.estimatedRowHeight
+ self.tableView.allowsSelection = true
+ self.tableView.register(cellType: OptionListViewCell.self)
+ self.tableView.tableFooterView = UIView()
+ }
+
+ private func render(viewState: OptionListViewState) {
+ switch viewState {
+ case .idle:
+ break
+ case .loading:
+ self.renderLoading()
+ case .loaded(let title, let options):
+ self.renderLoaded(title: title, options: options)
+ case .error(let error):
+ self.render(error: error)
+ }
+ }
+
+ private func renderLoading() {
+ self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
+ }
+
+ private func renderLoaded(title: String?, options: [OptionListItemViewData]) {
+ self.activityPresenter.removeCurrentActivityIndicator(animated: true)
+ self.titleLabel.text = title
+ self.options = options
+ }
+
+ private func render(error: Error) {
+ self.activityPresenter.removeCurrentActivityIndicator(animated: true)
+ self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
+ }
+
+
+ // MARK: - Actions
+
+ @IBAction private func closeAction(_ sender: Any) {
+ self.viewModel.process(viewAction: .cancel)
+ }
+}
+
+
+// MARK: - OptionListViewModelViewDelegate
+extension OptionListViewController: OptionListViewModelViewDelegate {
+
+ func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didUpdateViewState viewSate: OptionListViewState) {
+ self.render(viewState: viewSate)
+ }
+}
+
+// MARK: - SlidingModalPresentable
+extension OptionListViewController: SlidingModalPresentable {
+ func allowsDismissOnBackgroundTap() -> Bool {
+ return true
+ }
+
+ func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat {
+ return tableView.frame.minY + Constants.estimatedRowHeight * CGFloat(options.count) + bottomMargin.constant
+ }
+}
+
+// MARK: - UITableViewDataSource
+extension OptionListViewController: UITableViewDataSource {
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return options.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let viewData = options[indexPath.row]
+
+ let cell = tableView.dequeueReusableCell(for: indexPath, cellType: OptionListViewCell.self)
+ cell.update(theme: self.theme)
+ cell.update(with: viewData)
+
+ return cell
+ }
+}
+
+// MARK: - UITableViewDelegate
+extension OptionListViewController: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+ if options[indexPath.row].enabled {
+ viewModel.process(viewAction: .selected(indexPath.row))
+ }
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewModel.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewModel.swift
new file mode 100644
index 000000000..0cfe46c19
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewModel.swift
@@ -0,0 +1,66 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 Foundation
+
+final class OptionListViewModel: OptionListViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let title: String?
+ private let options: [OptionListItemViewData]
+
+ // MARK: Public
+
+ weak var viewDelegate: OptionListViewModelViewDelegate?
+ weak var coordinatorDelegate: OptionListViewModelCoordinatorDelegate?
+
+ private(set) var viewState: OptionListViewState = .idle {
+ didSet {
+ self.viewDelegate?.optionListViewModel(self, didUpdateViewState: viewState)
+ }
+ }
+
+ // MARK: - Setup
+
+ init(title: String?, options: [OptionListItemViewData]) {
+ self.title = title
+ self.options = options
+ }
+
+ // MARK: - Public
+
+ func process(viewAction: OptionListViewAction) {
+ switch viewAction {
+ case .loadData:
+ self.loadData()
+ case .selected(let index):
+ self.coordinatorDelegate?.optionListViewModel(self, didSelectOptionAt: index)
+ case .cancel:
+ self.coordinatorDelegate?.optionListViewModelDidCancel(self)
+ }
+ }
+
+ // MARK: - Private
+
+ private func loadData() {
+ self.viewState = .loaded(title, options)
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewModelProtocol.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewModelProtocol.swift
new file mode 100644
index 000000000..0d127ca05
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewModelProtocol.swift
@@ -0,0 +1,39 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 Foundation
+
+protocol OptionListViewModelViewDelegate: AnyObject {
+ func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didUpdateViewState viewSate: OptionListViewState)
+}
+
+protocol OptionListViewModelCoordinatorDelegate: AnyObject {
+ func optionListViewModel(_ viewModel: OptionListViewModelProtocol, didSelectOptionAt index: Int)
+ func optionListViewModelDidCancel(_ viewModel: OptionListViewModelProtocol)
+}
+
+/// Protocol describing the view model used by `OptionListViewController`
+protocol OptionListViewModelProtocol {
+
+ var viewDelegate: OptionListViewModelViewDelegate? { get set }
+ var coordinatorDelegate: OptionListViewModelCoordinatorDelegate? { get set }
+
+ func process(viewAction: OptionListViewAction)
+
+ var viewState: OptionListViewState { get }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewState.swift b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewState.swift
new file mode 100644
index 000000000..e64c36034
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/OptionList/OptionListViewState.swift
@@ -0,0 +1,27 @@
+// File created from ScreenTemplate
+// $ createScreen.sh Room/ParticipantsInviteModal/OptionList OptionList
+/*
+ Copyright 2021 New Vector Ltd
+
+ 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 Foundation
+
+/// OptionListViewController view state
+enum OptionListViewState {
+ case idle
+ case loading
+ case loaded(_ title: String?, _ options: [OptionListItemViewData])
+ case error(Error)
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/RoomParticipantsInviteModalCoordinatorBridgePresenter.swift b/Riot/Modules/Room/ParticipantsInviteModal/RoomParticipantsInviteModalCoordinatorBridgePresenter.swift
new file mode 100644
index 000000000..804c271b3
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/RoomParticipantsInviteModalCoordinatorBridgePresenter.swift
@@ -0,0 +1,214 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+
+@objc protocol RoomParticipantsInviteCoordinatorBridgePresenterDelegate {
+ func roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading(_ coordinatorBridgePresenter: RoomParticipantsInviteCoordinatorBridgePresenter)
+ func roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading(_ coordinatorBridgePresenter: RoomParticipantsInviteCoordinatorBridgePresenter)
+ func roomParticipantsInviteCoordinatorBridgePresenterDidComplete(_ coordinatorBridgePresenter: RoomParticipantsInviteCoordinatorBridgePresenter)
+}
+
+/// RoomParticipantsInviteCoordinatorBridgePresenter enables to start ContactsPickerCoordinator from a view controller.
+/// This bridge is used while waiting for global usage of coordinator pattern.
+@objcMembers
+final class RoomParticipantsInviteCoordinatorBridgePresenter: NSObject {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let session: MXSession?
+ private let room: MXRoom?
+ private let parentSpaceId: String?
+ private let currentSearchText: String?
+ private var actualParticipants: [Contact]?
+ private var invitedParticipants: [Contact]?
+ private var userParticipant: Contact?
+ private var roomOptions: [RoomOptionListItemViewData] = []
+
+ private weak var contactsPickerViewController: ContactsTableViewController?
+ private weak var currentAlert: UIAlertController?
+ private var contactPickerCoordinator: ContactsPickerCoordinator?
+ private var optionListCoordinator: OptionListCoordinator?
+ private var navigationRouter: NavigationRouterType?
+
+ // MARK: Public
+
+ weak var delegate: RoomParticipantsInviteCoordinatorBridgePresenterDelegate?
+
+ // MARK: - Setup
+
+ init(session: MXSession?, room: MXRoom?, parentSpaceId: String?) {
+ self.session = session
+ self.room = room
+ self.parentSpaceId = parentSpaceId
+ self.currentSearchText = nil
+ self.actualParticipants = nil
+ self.invitedParticipants = nil
+ self.userParticipant = nil
+
+ super.init()
+ }
+
+ init(session: MXSession?, room: MXRoom?, parentSpaceId: String?, currentSearchText: String? = nil, actualParticipants: [Contact]? = nil, invitedParticipants: [Contact]? = nil, userParticipant: Contact? = nil) {
+ self.session = session
+ self.room = room
+ self.parentSpaceId = parentSpaceId
+ self.currentSearchText = currentSearchText
+ self.actualParticipants = actualParticipants
+ self.invitedParticipants = invitedParticipants
+ self.userParticipant = userParticipant
+
+ super.init()
+ }
+
+ func present(from viewController: UIViewController, animated: Bool) {
+ guard let room = self.room else {
+ MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] present: nil room found")
+ return
+ }
+
+ if let navigationController = viewController.navigationController {
+ self.navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController)
+ } else {
+ self.navigationRouter = nil
+ }
+
+ if let spaceId = self.parentSpaceId, let spaceRoom = self.session?.spaceService.getSpace(withId: spaceId)?.room {
+ self.presentRoomSelector(between: room, and: spaceRoom)
+ return
+ }
+
+ self.pushContactsPicker(for: room)
+ }
+
+ // MARK: - Private
+
+ private class RoomOptionListItemViewData: OptionListItemViewData {
+ let room: MXRoom
+
+ init(title: String? = nil,
+ detail: String? = nil,
+ image: UIImage? = nil,
+ room: MXRoom,
+ accessoryImage: UIImage? = Asset.Images.chevron.image,
+ enabled: Bool = true) {
+ self.room = room
+ super.init(title: title, detail: detail, image: image, accessoryImage: accessoryImage, enabled: enabled)
+ }
+ }
+
+ private func presentRoomSelector(between room: MXRoom, and spaceRoom: MXRoom) {
+ let roomName = room.displayName ?? ""
+ let spaceName = spaceRoom.displayName ?? ""
+
+ self.roomOptions = [
+ RoomOptionListItemViewData(title: VectorL10n.roomInviteToSpaceOptionTitle(spaceName),
+ detail: VectorL10n.roomInviteToSpaceOptionDetail(spaceName, roomName),
+ image: Asset.Images.addParticipants.image, room: spaceRoom,
+ accessoryImage: Asset.Images.chevron.image),
+ RoomOptionListItemViewData(title: VectorL10n.roomInviteToRoomOptionTitle,
+ detail: VectorL10n.roomInviteToRoomOptionDetail(spaceName),
+ image: Asset.Images.addParticipants.image, room: room,
+ accessoryImage: Asset.Images.chevron.image)
+ ]
+
+ let coordinator = OptionListCoordinator(parameters: OptionListCoordinatorParameters(title: VectorL10n.roomIntroCellAddParticipantsAction, options: self.roomOptions, navigationRouter: self.navigationRouter))
+ coordinator.delegate = self
+ coordinator.start()
+
+ self.optionListCoordinator = coordinator
+ }
+
+ private func pushContactsPicker(for room: MXRoom) {
+ guard let session = self.session else {
+ MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] pushContactsPicker: nil session found")
+ return
+ }
+
+ canInvite(to: room) { [weak self] canInvite in
+ guard let self = self else { return }
+
+ guard canInvite else {
+ let message = room.summary?.roomType == .space ? VectorL10n.spaceInviteNotEnoughPermission : VectorL10n.roomInviteNotEnoughPermission
+ let alert = UIAlertController(title: VectorL10n.spacesInvitePeople, message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil))
+ self.navigationRouter?.present(alert, animated: true)
+ return
+ }
+
+ let coordinator = ContactsPickerCoordinator(session: session,
+ room: room,
+ initialSearchText: self.currentSearchText,
+ actualParticipants: self.actualParticipants,
+ invitedParticipants: self.invitedParticipants,
+ userParticipant: self.userParticipant,
+ navigationRouter: self.navigationRouter)
+ coordinator.delegate = self
+ coordinator.start()
+
+ self.contactPickerCoordinator = coordinator
+ }
+ }
+
+ private func canInvite(to room: MXRoom, completion: @escaping (Bool) -> Void) {
+ guard let userId = self.session?.myUserId else {
+ MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] canInvite: userId not found")
+ completion(false)
+ return
+ }
+
+ room.state { roomState in
+ guard let powerLevels = roomState?.powerLevels else {
+ MXLog.error("[RoomParticipantsInviteCoordinatorBridgePresenter] canInvite: room powerLevels not found")
+ completion(false)
+ return
+ }
+ let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: userId)
+
+ completion(userPowerLevel >= powerLevels.invite)
+ }
+ }
+}
+
+extension RoomParticipantsInviteCoordinatorBridgePresenter: ContactsPickerCoordinatorDelegate {
+ func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ delegate?.roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading(self)
+ }
+
+ func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ delegate?.roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading(self)
+ }
+
+ func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ delegate?.roomParticipantsInviteCoordinatorBridgePresenterDidComplete(self)
+ }
+}
+
+extension RoomParticipantsInviteCoordinatorBridgePresenter: OptionListCoordinatorDelegate {
+ func optionListCoordinator(_ coordinator: OptionListCoordinatorProtocol, didSelectOptionAt index: Int) {
+ optionListCoordinator = nil
+ self.pushContactsPicker(for: roomOptions[index].room)
+ }
+
+ func optionListCoordinator(_ coordinator: OptionListCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) {
+ optionListCoordinator = nil
+ }
+
+ func optionListCoordinatorDidCancel(_ coordinator: OptionListCoordinatorProtocol) {
+ optionListCoordinator = nil
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkHeaderView.swift b/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkHeaderView.swift
new file mode 100644
index 000000000..11d9f5394
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkHeaderView.swift
@@ -0,0 +1,77 @@
+//
+// Copyright 2020 New Vector Ltd
+//
+// 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 UIKit
+import Reusable
+
+@objc
+protocol ShareInviteLinkHeaderViewDelegate: AnyObject {
+ func shareInviteLinkHeaderView(_ headerView: ShareInviteLinkHeaderView, didTapButton button: UIButton)
+}
+
+@objcMembers
+final class ShareInviteLinkHeaderView: UIView, NibLoadable, Themable {
+
+ // MARK: - Constants
+
+ private enum Constants {
+ static let buttonHighlightedAlpha: CGFloat = 0.2
+ }
+
+ // MARK: - Properties
+
+ @IBOutlet private weak var button: CustomRoundedButton!
+
+ weak var delegate: ShareInviteLinkHeaderViewDelegate?
+
+ // MARK: - Setup
+
+ static func instantiate() -> ShareInviteLinkHeaderView {
+ let view = ShareInviteLinkHeaderView.loadFromNib()
+ view.update(theme: ThemeService.shared().theme)
+ return view
+ }
+
+ // MARK: - Life cycle
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ button.setTitle(VectorL10n.shareInviteLinkAction, for: .normal)
+ button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
+ button.layer.cornerRadius = 8
+ button.layer.borderWidth = 2
+ }
+
+ // MARK: - Public
+
+ func update(theme: Theme) {
+ button.layer.borderColor = theme.tintColor.cgColor
+ button.setTitleColor(theme.tintColor, for: .normal)
+ button.setTitleColor(theme.tintColor.withAlphaComponent(Constants.buttonHighlightedAlpha), for: .highlighted)
+ button.vc_setBackgroundColor(theme.baseColor, for: .normal)
+
+ let buttonImage = Asset.Images.shareActionButton.image.vc_tintedImage(usingColor: theme.tintColor)
+
+ button.setImage(buttonImage, for: .normal)
+ }
+
+ // MARK: - Action
+
+ @objc private func buttonAction(_ sender: UIButton) {
+ delegate?.shareInviteLinkHeaderView(self, didTapButton: button)
+ }
+}
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkHeaderView.xib b/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkHeaderView.xib
new file mode 100644
index 000000000..546eb5e93
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkHeaderView.xib
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkPresenter.swift b/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkPresenter.swift
new file mode 100644
index 000000000..8cbfead9d
--- /dev/null
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ShareInviteLink/ShareInviteLinkPresenter.swift
@@ -0,0 +1,87 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 Foundation
+
+/// ShareInviteLinkPresenter enables to share room alias to someone else
+@objcMembers
+final class ShareInviteLinkPresenter: NSObject {
+
+ // MARK: - Constants
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private weak var presentingViewController: UIViewController?
+ private weak var sourceView: UIView?
+
+ // MARK: - Public
+
+ func present(for room: MXRoom,
+ from viewController: UIViewController,
+ sourceView: UIView?,
+ animated: Bool) {
+
+ self.presentingViewController = viewController
+ self.sourceView = sourceView
+
+ self.shareInvite(from: room)
+ }
+
+ func dismiss(animated: Bool, completion: (() -> Void)?) {
+ self.presentingViewController?.dismiss(animated: animated, completion: completion)
+ }
+
+ // MARK: - Private
+
+ private func shareInvite(from room: MXRoom) {
+
+ let shareText = self.buildShareText(with: room)
+
+ // Set up activity view controller
+ let activityItems: [Any] = [ shareText ]
+ let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
+
+ self.present(activityViewController, animated: true)
+ }
+
+ private func buildShareText(with room: MXRoom) -> String {
+ let roomAliasOrId: String
+ if let alias = room.summary?.aliases?.first {
+ roomAliasOrId = alias
+ } else {
+ roomAliasOrId = room.matrixItemId
+ }
+
+ if room.summary?.roomType == .space {
+ return VectorL10n.shareInviteLinkSpaceText(MXTools.permalink(toRoom: roomAliasOrId))
+ } else {
+ return VectorL10n.shareInviteLinkRoomText(MXTools.permalink(toRoom: roomAliasOrId))
+ }
+ }
+
+ private func present(_ viewController: UIViewController, animated: Bool) {
+
+ // Configure source view when view controller is presented with a popover
+ if let sourceView = self.sourceView, let popoverPresentationController = viewController.popoverPresentationController {
+ popoverPresentationController.sourceView = sourceView
+ popoverPresentationController.sourceRect = sourceView.bounds
+ }
+
+ self.presentingViewController?.present(viewController, animated: animated, completion: nil)
+ }
+}
diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift
index b28e1c58b..c3799610a 100644
--- a/Riot/Modules/Room/RoomCoordinator.swift
+++ b/Riot/Modules/Room/RoomCoordinator.swift
@@ -80,8 +80,11 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
} else {
self.roomViewController = RoomViewController.instantiate(with: parameters.displayConfiguration)
}
+ self.roomViewController.showSettingsInitially = parameters.showSettingsInitially
self.activityIndicatorPresenter = ActivityIndicatorPresenter()
+ self.roomViewController.parentSpaceId = parameters.parentSpaceId
+
if #available(iOS 14, *) {
TimelinePollProvider.shared.session = parameters.session
}
@@ -365,6 +368,10 @@ extension RoomCoordinator: RoomViewControllerDelegate {
self.delegate?.roomCoordinator(self, didSelectRoomWithId: roomID, eventId: eventID)
}
+ func roomViewController(_ roomViewController: RoomViewController, didReplaceRoomWithReplacementId roomID: String) {
+ self.delegate?.roomCoordinator(self, didReplaceRoomWithReplacementId: roomID)
+ }
+
func roomViewController(_ roomViewController: RoomViewController, showMemberDetails roomMember: MXRoomMember) {
// TODO:
}
@@ -452,4 +459,12 @@ extension RoomCoordinator: RoomViewControllerDelegate {
func roomViewControllerDidStopLoading(_ roomViewController: RoomViewController) {
stopLoading()
}
+
+ func roomViewControllerDidTapLiveLocationSharingBanner(_ roomViewController: RoomViewController) {
+ // TODO:
+ }
+
+ func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController) {
+ // TODO:
+ }
}
diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift
index 017b8b6e8..14a206214 100644
--- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift
+++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift
@@ -21,6 +21,7 @@ import Foundation
func roomCoordinatorBridgePresenter(_ bridgePresenter: RoomCoordinatorBridgePresenter,
didSelectRoomWithId roomId: String,
eventId: String?)
+ func roomCoordinatorBridgePresenter(_ bridgePresenter: RoomCoordinatorBridgePresenter, didReplaceRoomWithReplacementId roomId: String)
func roomCoordinatorBridgePresenterDidDismissInteractively(_ bridgePresenter: RoomCoordinatorBridgePresenter)
}
@@ -33,6 +34,9 @@ class RoomCoordinatorBridgePresenterParameters: NSObject {
/// The room identifier
let roomId: String
+ /// The identifier of the parent space. `nil` for home space
+ let parentSpaceId: String?
+
/// If not nil, the room will be opened on this event.
let eventId: String?
@@ -45,18 +49,25 @@ class RoomCoordinatorBridgePresenterParameters: NSObject {
/// The data for the room preview.
let previewData: RoomPreviewData?
+ /// If `true`, the room settings screen will be initially displayed. Default `false`
+ let showSettingsInitially: Bool
+
init(session: MXSession,
roomId: String,
+ parentSpaceId: String?,
eventId: String?,
threadId: String?,
displayConfiguration: RoomDisplayConfiguration,
- previewData: RoomPreviewData?) {
+ previewData: RoomPreviewData?,
+ showSettingsInitially: Bool) {
self.session = session
self.roomId = roomId
+ self.parentSpaceId = parentSpaceId
self.eventId = eventId
self.threadId = threadId
self.displayConfiguration = displayConfiguration
self.previewData = previewData
+ self.showSettingsInitially = showSettingsInitially
}
}
@@ -94,7 +105,7 @@ final class RoomCoordinatorBridgePresenter: NSObject {
// MARK: - Public
func present(from viewController: UIViewController, animated: Bool) {
- let coordinator = self.createRoomCoordinator()
+ let coordinator = self.createRoomCoordinator(parentSpaceId: bridgeParameters.parentSpaceId)
coordinator.delegate = self
let presentable = coordinator.toPresentable()
presentable.modalPresentationStyle = .formSheet
@@ -109,7 +120,7 @@ final class RoomCoordinatorBridgePresenter: NSObject {
let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController)
- let coordinator = self.createRoomCoordinator(with: navigationRouter)
+ let coordinator = self.createRoomCoordinator(with: navigationRouter, parentSpaceId: bridgeParameters.parentSpaceId)
coordinator.delegate = self
coordinator.start() // Will trigger view controller push
@@ -141,18 +152,20 @@ final class RoomCoordinatorBridgePresenter: NSObject {
// MARK: - Private
- private func createRoomCoordinator(with navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) -> RoomCoordinator {
+ private func createRoomCoordinator(with navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController()), parentSpaceId: String?) -> RoomCoordinator {
let coordinatorParameters: RoomCoordinatorParameters
if let previewData = self.bridgeParameters.previewData {
- coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, previewData: previewData)
+ coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, parentSpaceId: parentSpaceId, previewData: previewData)
} else {
coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter,
session: self.bridgeParameters.session,
+ parentSpaceId: parentSpaceId,
roomId: self.bridgeParameters.roomId,
eventId: self.bridgeParameters.eventId,
threadId: self.bridgeParameters.threadId,
+ showSettingsInitially: self.bridgeParameters.showSettingsInitially,
displayConfiguration: self.bridgeParameters.displayConfiguration)
}
@@ -167,6 +180,10 @@ extension RoomCoordinatorBridgePresenter: RoomCoordinatorDelegate {
self.delegate?.roomCoordinatorBridgePresenter(self, didSelectRoomWithId: roomId, eventId: eventId)
}
+ func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didReplaceRoomWithReplacementId roomId: String) {
+ self.delegate?.roomCoordinatorBridgePresenter(self, didReplaceRoomWithReplacementId: roomId)
+ }
+
func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) {
self.delegate?.roomCoordinatorBridgePresenterDidLeaveRoom(self)
}
diff --git a/Riot/Modules/Room/RoomCoordinatorParameters.swift b/Riot/Modules/Room/RoomCoordinatorParameters.swift
index ae31416e8..1121c0990 100644
--- a/Riot/Modules/Room/RoomCoordinatorParameters.swift
+++ b/Riot/Modules/Room/RoomCoordinatorParameters.swift
@@ -37,6 +37,9 @@ struct RoomCoordinatorParameters {
/// The room identifier
let roomId: String
+ /// The identifier of the parent space. `nil` for home space
+ let parentSpaceId: String?
+
/// If not nil, the room will be opened on this event.
let eventId: String?
@@ -49,6 +52,9 @@ struct RoomCoordinatorParameters {
/// The data for the room preview.
let previewData: RoomPreviewData?
+ /// If `true`, the room settings screen will be initially displayed. Default `false`
+ let showSettingsInitially: Bool
+
// MARK: - Setup
private init(navigationRouter: NavigationRouterType?,
@@ -56,19 +62,23 @@ struct RoomCoordinatorParameters {
userIndicatorPresenter: UserIndicatorTypePresenterProtocol?,
session: MXSession,
roomId: String,
+ parentSpaceId: String?,
eventId: String?,
threadId: String?,
displayConfiguration: RoomDisplayConfiguration,
- previewData: RoomPreviewData?) {
+ previewData: RoomPreviewData?,
+ showSettingsInitially: Bool) {
self.navigationRouter = navigationRouter
self.navigationRouterStore = navigationRouterStore
self.userIndicatorPresenter = userIndicatorPresenter
self.session = session
self.roomId = roomId
+ self.parentSpaceId = parentSpaceId
self.eventId = eventId
self.threadId = threadId
self.displayConfiguration = displayConfiguration
self.previewData = previewData
+ self.showSettingsInitially = showSettingsInitially
}
/// Init to present a joined room
@@ -76,9 +86,11 @@ struct RoomCoordinatorParameters {
navigationRouterStore: NavigationRouterStoreProtocol? = nil,
userIndicatorPresenter: UserIndicatorTypePresenterProtocol? = nil,
session: MXSession,
+ parentSpaceId: String?,
roomId: String,
eventId: String? = nil,
threadId: String? = nil,
+ showSettingsInitially: Bool,
displayConfiguration: RoomDisplayConfiguration = .default) {
self.init(navigationRouter: navigationRouter,
@@ -86,15 +98,18 @@ struct RoomCoordinatorParameters {
userIndicatorPresenter: userIndicatorPresenter,
session: session,
roomId: roomId,
+ parentSpaceId: parentSpaceId,
eventId: eventId,
threadId: threadId,
displayConfiguration: displayConfiguration,
- previewData: nil)
+ previewData: nil,
+ showSettingsInitially: showSettingsInitially)
}
/// Init to present a room preview
init(navigationRouter: NavigationRouterType? = nil,
navigationRouterStore: NavigationRouterStoreProtocol? = nil,
+ parentSpaceId: String?,
previewData: RoomPreviewData) {
self.init(navigationRouter: navigationRouter,
@@ -102,9 +117,11 @@ struct RoomCoordinatorParameters {
userIndicatorPresenter: nil,
session: previewData.mxSession,
roomId: previewData.roomId,
+ parentSpaceId: parentSpaceId,
eventId: nil,
threadId: nil,
displayConfiguration: .default,
- previewData: previewData)
+ previewData: previewData,
+ showSettingsInitially: false)
}
}
diff --git a/Riot/Modules/Room/RoomCoordinatorProtocol.swift b/Riot/Modules/Room/RoomCoordinatorProtocol.swift
index 9fe774ce6..6b0d96813 100644
--- a/Riot/Modules/Room/RoomCoordinatorProtocol.swift
+++ b/Riot/Modules/Room/RoomCoordinatorProtocol.swift
@@ -22,6 +22,7 @@ protocol RoomCoordinatorDelegate: AnyObject {
func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol)
func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol)
func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String, eventId: String?)
+ func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didReplaceRoomWithReplacementId roomId: String)
func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol)
}
diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift
index ed5637e96..872406499 100644
--- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift
+++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift
@@ -28,7 +28,9 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
private let navigationRouter: NavigationRouterType
private let session: MXSession
private let room: MXRoom
+ private let parentSpaceId: String?
private let initialSection: RoomInfoSection
+ private let dismissOnCancel: Bool
private weak var roomSettingsViewController: RoomSettingsViewController?
private lazy var segmentedViewController: SegmentedViewController = {
@@ -38,6 +40,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
participants.finalizeInit()
participants.enableMention = true
participants.mxRoom = self.room
+ participants.parentSpaceId = self.parentSpaceId
participants.delegate = self
participants.screenTracker = AnalyticsScreenTracker(screen: .roomMembers)
@@ -53,6 +56,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
}
let settings = RoomSettingsViewController()
+ settings.delegate = self
settings.finalizeInit()
settings.screenTracker = AnalyticsScreenTracker(screen: .roomSettings)
settings.initWith(self.session, andRoomId: self.room.roomId)
@@ -98,7 +102,9 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
self.session = parameters.session
self.room = parameters.room
+ self.parentSpaceId = parameters.parentSpaceId
self.initialSection = parameters.initialSection
+ self.dismissOnCancel = parameters.dismissOnCancel
}
// MARK: - Public methods
@@ -111,7 +117,9 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
self.add(childCoordinator: rootCoordinator)
if self.navigationRouter.modules.isEmpty == false {
- self.navigationRouter.push(rootCoordinator.toPresentable(), animated: true, popCompletion: nil)
+ // push room info screen non animated if another screen needs to be pushed just after
+ let animated = initialSection == .none
+ self.navigationRouter.push(rootCoordinator.toPresentable(), animated: animated, popCompletion: nil)
} else {
self.navigationRouter.setRootModule(rootCoordinator)
}
@@ -123,6 +131,8 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
self.showRoomDetails(with: .settings(RoomSettingsViewControllerFieldAvatar), animated: false)
case .changeTopic:
self.showRoomDetails(with: .settings(RoomSettingsViewControllerFieldTopic), animated: false)
+ case .settings:
+ self.showRoomDetails(with: .settings(RoomSettingsViewControllerFieldNone), animated: false)
case .none:
break
}
@@ -216,3 +226,29 @@ extension RoomInfoCoordinator: RoomNotificationSettingsCoordinatorDelegate {
}
}
+
+extension RoomInfoCoordinator: RoomSettingsViewControllerDelegate {
+ func roomSettingsViewControllerDidCancel(_ controller: RoomSettingsViewController!) {
+ if self.dismissOnCancel {
+ self.navigationRouter.dismissModule(animated: true, completion: nil)
+ } else {
+ controller.withdrawViewController(animated: true) {}
+ }
+ }
+
+ func roomSettingsViewControllerDidComplete(_ controller: RoomSettingsViewController!) {
+ if self.dismissOnCancel {
+ self.navigationRouter.dismissModule(animated: true, completion: nil)
+ } else {
+ controller.withdrawViewController(animated: true) {}
+ }
+ }
+
+ func roomSettingsViewController(_ controller: RoomSettingsViewController!, didReplaceRoomWithReplacementId newRoomId: String!) {
+ self.delegate?.roomInfoCoordinator(self, didReplaceRoomWithReplacementId: newRoomId)
+ }
+
+ func roomSettingsViewControllerDidLeaveRoom(_ controller: RoomSettingsViewController!) {
+ self.delegate?.roomInfoCoordinatorDidLeaveRoom(self)
+ }
+}
diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift
index ae71c988b..b8db2a66a 100644
--- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift
+++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift
@@ -22,6 +22,7 @@ import Foundation
func roomInfoCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter)
func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didRequestMentionForMember member: MXRoomMember)
func roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter)
+ func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didReplaceRoomWithReplacementId roomId: String)
}
/// RoomInfoCoordinatorBridgePresenter enables to start RoomInfoCoordinator from a view controller.
@@ -124,6 +125,10 @@ extension RoomInfoCoordinatorBridgePresenter: RoomInfoCoordinatorDelegate {
func roomInfoCoordinatorDidLeaveRoom(_ coordinator: RoomInfoCoordinatorType) {
self.delegate?.roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom(self)
}
+
+ func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) {
+ self.delegate?.roomInfoCoordinatorBridgePresenter(self, didReplaceRoomWithReplacementId: roomId)
+ }
}
// MARK: - UIAdaptivePresentationControllerDelegate
diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift
index 42a4aab5b..c1c46fab4 100644
--- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift
+++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift
@@ -20,6 +20,7 @@ import Foundation
enum RoomInfoSection: Int {
case none
case addParticipants
+ case settings
case changeAvatar
case changeTopic
}
@@ -29,16 +30,24 @@ class RoomInfoCoordinatorParameters: NSObject {
let session: MXSession
let room: MXRoom
+ let parentSpaceId: String?
let initialSection: RoomInfoSection
+ let dismissOnCancel: Bool
- init(session: MXSession, room: MXRoom, initialSection: RoomInfoSection) {
+ init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, dismissOnCancel: Bool) {
self.session = session
self.room = room
+ self.parentSpaceId = parentSpaceId
self.initialSection = initialSection
+ self.dismissOnCancel = dismissOnCancel
super.init()
}
- convenience init(session: MXSession, room: MXRoom) {
- self.init(session: session, room: room, initialSection: .none)
+ convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?) {
+ self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: .none, dismissOnCancel: false)
+ }
+
+ convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection) {
+ self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, dismissOnCancel: false)
}
}
diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift
index 41065a236..80f696b0b 100644
--- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift
+++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift
@@ -22,6 +22,7 @@ protocol RoomInfoCoordinatorDelegate: AnyObject {
func roomInfoCoordinatorDidComplete(_ coordinator: RoomInfoCoordinatorType)
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember)
func roomInfoCoordinatorDidLeaveRoom(_ coordinator: RoomInfoCoordinatorType)
+ func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String)
}
/// `RoomInfoCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow.
diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h
index 1513bafb5..1dcb69ece 100644
--- a/Riot/Modules/Room/RoomViewController.h
+++ b/Riot/Modules/Room/RoomViewController.h
@@ -84,6 +84,11 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification;
*/
@property (nonatomic) BOOL showMissedDiscussionsBadge;
+/**
+ ID of the parent space. `nil` for home space.
+ */
+@property (nonatomic, nullable) NSString *parentSpaceId;
+
/**
Display the preview of a room that is unknown for the user.
@@ -93,6 +98,11 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification;
*/
- (void)displayRoomPreview:(RoomPreviewData*)roomPreviewData;
+/**
+ If `YES`, the room settings screen will be initially displayed. Default `NO`
+ */
+@property (nonatomic) BOOL showSettingsInitially;
+
/**
Action used to handle some buttons.
*/
@@ -151,6 +161,15 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification;
showRoomWithId:(NSString *)roomID
eventId:(nullable NSString *)eventID;
+/**
+ Tells the delegate that the room has replaced by a room with a specific replacement room ID.
+
+ @param roomViewController the `RoomViewController` instance.
+ @param roomID the replacement roomId
+ */
+- (void)roomViewController:(RoomViewController *)roomViewController
+didReplaceRoomWithReplacementId:(NSString *)roomID;
+
/**
Tells the delegate that the user wants to start a direct chat with a user.
@@ -258,6 +277,12 @@ didRequestEditForPollWithStartEvent:(MXEvent *)startEvent;
*/
- (void)roomViewControllerDidStopLoading:(RoomViewController *)roomViewController;
+/// User tap live location sharing stop action
+- (void)roomViewControllerDidStopLiveLocationSharing:(RoomViewController *)roomViewController;
+
+/// User tap live location sharing banner
+- (void)roomViewControllerDidTapLiveLocationSharingBanner:(RoomViewController *)roomViewController;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m
index d8a02a9ae..f0ff98748 100644
--- a/Riot/Modules/Room/RoomViewController.m
+++ b/Riot/Modules/Room/RoomViewController.m
@@ -89,11 +89,15 @@
NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification";
NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification";
const NSTimeInterval kResizeComposerAnimationDuration = .05;
+static const int kThreadListBarButtonItemTag = 99;
+static UIEdgeInsets kThreadListBarButtonItemContentInsetsNoDot;
+static UIEdgeInsets kThreadListBarButtonItemContentInsetsDot;
+static CGSize kThreadListBarButtonItemImageSize;
@interface RoomViewController ()
+ RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate>
{
// The preview header
@@ -155,6 +159,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// Tell whether the view controller is appeared or not.
BOOL isAppeared;
+ // A flag indicating whether a room has been left
+ BOOL isRoomLeft;
+
// Tell whether the room has a Jitsi call or not.
BOOL hasJitsiCall;
@@ -178,9 +185,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// Time to display notification content in the timeline
MXTaskProfile *notificationTaskProfile;
-
- // Reference to thread list bar button item, to update it easily later
- BadgedBarButtonItem *threadListBarButtonItem;
}
@property (nonatomic, weak) IBOutlet UIView *overlayContainerView;
@@ -201,6 +205,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
@property (nonatomic, strong) RoomCreationModalCoordinatorBridgePresenter *roomCreationModalCoordinatorBridgePresenter;
@property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter;
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
+@property (nonatomic, strong) RoomParticipantsInviteCoordinatorBridgePresenter *participantsInvitePresenter;
@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter;
@property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded;
@property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden;
@@ -222,6 +227,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// scroll state just before the layout change, and restore it after the layout.
@property (nonatomic) BOOL shouldScrollToBottomAfterLayout;
+/// Handles all banners that should be displayed at the top of the timeline but that should not scroll with the timeline
+@property (weak, nonatomic, nullable) IBOutlet UIStackView *topBannersStackView;
+
+@property (nonatomic) BOOL shouldShowLiveLocationSharingBannerView;
+
+@property (nonatomic, weak) LiveLocationSharingBannerView *liveLocationSharingBannerView;
+
@end
@implementation RoomViewController
@@ -229,6 +241,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
#pragma mark - Class methods
++ (void)initialize
+{
+ kThreadListBarButtonItemContentInsetsNoDot = UIEdgeInsetsMake(0, 8, 0, 8);
+ kThreadListBarButtonItemContentInsetsDot = UIEdgeInsetsMake(0, 8, 6, 8);
+ kThreadListBarButtonItemImageSize = CGSizeMake(21, 21);
+}
+
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass(self.class)
@@ -384,6 +403,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[self registerURLPreviewNotifications];
[self setupActions];
+
+ [self setupUserSuggestionViewIfNeeded];
}
- (void)userInterfaceThemeDidChange
@@ -460,7 +481,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
self.scrollToBottomBadgeLabel.badgeColor = ThemeService.shared.theme.tintColor;
[self updateThreadListBarButtonBadgeWith:self.mainSession.threadingService];
- [threadListBarButtonItem updateWithTheme:ThemeService.shared.theme];
+
+ [self.liveLocationSharingBannerView updateWithTheme:ThemeService.shared.theme];
[self setNeedsStatusBarAppearanceUpdate];
}
@@ -631,6 +653,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
hasJitsiCall = NO;
[self reloadBubblesTable:YES];
}
+
+ self.showSettingsInitially = NO;
}
- (void)viewDidDisappear:(BOOL)animated
@@ -1026,7 +1050,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
room:dataSource.room];
_userSuggestionCoordinator.delegate = self;
- [self setupUserSuggestionView];
+ [self setupUserSuggestionViewIfNeeded];
}
- (void)onRoomDataSourceReady
@@ -1217,6 +1241,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// Check
if (roomAlias.length)
{
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerSlashCommand;
+
// TODO: /join command does not support via parameters yet
[self.mainSession joinRoom:roomAlias viaServers:nil success:^(MXRoom *room) {
@@ -1274,6 +1300,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (!bubbleTableViewDisplayInTransition && !self.bubblesTableView.isHidden)
{
[self refreshActivitiesViewDisplay];
+ [self refreshRoomTitle];
[self checkReadMarkerVisibility];
[self refreshJumpToLastUnreadBannerDisplay];
@@ -1514,20 +1541,21 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
return item;
}
-- (BadgedBarButtonItem *)threadListBarButtonItem
+- (UIBarButtonItem *)threadListBarButtonItem
{
UIButton *button = [UIButton new];
- UIImage *icon = [AssetImages.threadsIcon.image vc_resizedWith:CGSizeMake(21, 21)];
- button.contentEdgeInsets = UIEdgeInsetsMake(4, 8, 4, 8);
- [button setImage:icon
+ button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot;
+ button.imageView.contentMode = UIViewContentModeScaleAspectFit;
+ [button setImage:[AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize]
forState:UIControlStateNormal];
[button addTarget:self
action:@selector(onThreadListTapped:)
forControlEvents:UIControlEventTouchUpInside];
button.accessibilityLabel = [VectorL10n roomAccessibilityThreads];
-
- return [[BadgedBarButtonItem alloc] initWithBaseButton:button
- theme:ThemeService.shared.theme];
+
+ UIBarButtonItem *result = [[UIBarButtonItem alloc] initWithCustomView:button];
+ result.tag = kThreadListBarButtonItemTag;
+ return result;
}
- (void)setupRemoveJitsiWidgetRemoveView
@@ -1777,10 +1805,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
else
{
// in a regular timeline
- BadgedBarButtonItem *itemThreadList = [self threadListBarButtonItem];
+ UIBarButtonItem *itemThreadList = [self threadListBarButtonItem];
+ [self updateThreadListBarButtonItem:itemThreadList
+ with:self.mainSession.threadingService];
[rightBarButtonItems insertObject:itemThreadList atIndex:0];
- threadListBarButtonItem = itemThreadList;
- [self updateThreadListBarButtonBadgeWith:self.mainSession.threadingService];
}
}
}
@@ -2013,32 +2041,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
- (void)showRoomAvatarChange
{
- [self showRoomInfoWithInitialSection:RoomInfoSectionChangeAvatar];
+ [self showRoomInfoWithInitialSection:RoomInfoSectionChangeAvatar animated:YES];
}
- (void)showAddParticipants
{
- [self showRoomInfoWithInitialSection:RoomInfoSectionAddParticipants];
+ self.participantsInvitePresenter = [[RoomParticipantsInviteCoordinatorBridgePresenter alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId];
+ self.participantsInvitePresenter.delegate = self;
+ [self.participantsInvitePresenter presentFrom:self animated:YES];
}
- (void)showRoomTopicChange
{
- [self showRoomInfoWithInitialSection:RoomInfoSectionChangeTopic];
+ [self showRoomInfoWithInitialSection:RoomInfoSectionChangeTopic animated:YES];
}
- (void)showRoomInfo
{
- [self showRoomInfoWithInitialSection:RoomInfoSectionNone];
+ [self showRoomInfoWithInitialSection:RoomInfoSectionNone animated:YES];
}
-- (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection
+- (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection animated:(BOOL)animated
{
- RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room initialSection:roomInfoSection];
+ RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection];
self.roomInfoCoordinatorBridgePresenter = [[RoomInfoCoordinatorBridgePresenter alloc] initWithParameters:parameters];
self.roomInfoCoordinatorBridgePresenter.delegate = self;
- [self.roomInfoCoordinatorBridgePresenter pushFrom:self.navigationController animated:YES];
+ [self.roomInfoCoordinatorBridgePresenter pushFrom:self.navigationController animated:animated];
}
- (void)setupActions {
@@ -2268,14 +2298,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}
- (void)notifyDelegateOnLeaveRoomIfNecessary {
+ if (isRoomLeft) {
+ return;
+ }
+ isRoomLeft = YES;
+
if (self.delegate)
{
- // Leaving room often triggers multiple events, incl local delegate callbacks as well as global notifications,
- // which may lead to multiple identical UI changes (navigating to home, displaying notification etc).
- // To avoid this, as soon as we notify the delegate the first time, we nilify it, preventing future messages
- // from being passed along, assuming that after leaving a room there is nothing else to communicate to the delegate.
[self.delegate roomViewControllerDidLeaveRoom:self];
- self.delegate = nil;
}
else
{
@@ -2338,6 +2368,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters
{
+ Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerTimeline;
+
if (self.delegate)
{
return [self.delegate roomViewController:self handleUniversalLinkWithParameters:parameters];
@@ -2348,10 +2380,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}
}
-- (void)setupUserSuggestionView
+- (void)setupUserSuggestionViewIfNeeded
{
if(!self.isViewLoaded) {
- MXLogError(@"Failed setting up user suggestions. View not loaded.");
return;
}
@@ -2375,6 +2406,18 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[suggestionsViewController didMoveToParentViewController:self];
}
+- (void)updateTopBanners
+{
+ [self.view bringSubviewToFront:self.topBannersStackView];
+
+ [self.topBannersStackView vc_removeAllSubviews];
+
+ if (self.shouldShowLiveLocationSharingBannerView)
+ {
+ [self showLiveLocationBannerView];
+ }
+}
+
#pragma mark - Jitsi
- (void)showJitsiCallWithWidget:(Widget*)widget
@@ -3124,6 +3167,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
if (predecessorRoomId)
{
// Show predecessor room
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerTombstone;
[self showRoomWithId:predecessorRoomId];
}
else
@@ -6825,41 +6869,82 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
- (void)updateThreadListBarButtonBadgeWith:(MXThreadingService *)service
{
- if (!threadListBarButtonItem || !service)
+ [self updateThreadListBarButtonItem:nil with:service];
+}
+
+- (void)updateThreadListBarButtonItem:(UIBarButtonItem *)barButtonItem with:(MXThreadingService *)service
+{
+ if (!service)
{
- // there is no thread list bar button, ignore
return;
}
+
+ __block NSInteger replaceIndex = NSNotFound;
+ [self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem * _Nonnull item, NSUInteger index, BOOL * _Nonnull stop)
+ {
+ if (item.tag == kThreadListBarButtonItemTag)
+ {
+ replaceIndex = index;
+ *stop = YES;
+ }
+ }];
+
+ if (!barButtonItem && replaceIndex == NSNotFound)
+ {
+ // there is no thread list bar button item, and not provided another to update
+ // ignore
+ return;
+ }
+
+ UIBarButtonItem *threadListBarButtonItem = barButtonItem ?: [self threadListBarButtonItem];
+ UIButton *button = (UIButton *)threadListBarButtonItem.customView;
MXThreadNotificationsCount *notificationsCount = [service notificationsCountForRoom:self.roomDataSource.roomId];
if (notificationsCount.numberOfHighlightedThreads > 0)
{
- threadListBarButtonItem.badgeText = [self threadListBadgeTextFor:notificationsCount.numberOfHighlightedThreads];
- threadListBarButtonItem.badgeBackgroundColor = ThemeService.shared.theme.colors.alert;
+ [button setImage:AssetImages.threadsIconRedDot.image
+ forState:UIControlStateNormal];
+ button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsDot;
}
else if (notificationsCount.numberOfNotifiedThreads > 0)
{
- threadListBarButtonItem.badgeText = [self threadListBadgeTextFor:notificationsCount.numberOfNotifiedThreads];
- threadListBarButtonItem.badgeBackgroundColor = ThemeService.shared.theme.noticeSecondaryColor;
+ if (ThemeService.shared.isCurrentThemeDark)
+ {
+ [button setImage:AssetImages.threadsIconGrayDotDark.image
+ forState:UIControlStateNormal];
+ }
+ else
+ {
+ [button setImage:AssetImages.threadsIconGrayDotLight.image
+ forState:UIControlStateNormal];
+ }
+ button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsDot;
}
else
{
- // remove badge
- threadListBarButtonItem.badgeText = nil;
+ [button setImage:[AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize]
+ forState:UIControlStateNormal];
+ button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot;
}
-}
-- (NSString *)threadListBadgeTextFor:(NSUInteger)numberOfThreads
-{
- if (numberOfThreads < 100)
+ if (replaceIndex == NSNotFound)
{
- return [NSString stringWithFormat:@"%tu", numberOfThreads];
+ // there is no thread list bar button item, this was only an update
+ return;
}
- else
+
+ UIBarButtonItem *originalItem = self.navigationItem.rightBarButtonItems[replaceIndex];
+ UIButton *originalButton = (UIButton *)originalItem.customView;
+ if ([originalButton imageForState:UIControlStateNormal] == [button imageForState:UIControlStateNormal]
+ && UIEdgeInsetsEqualToEdgeInsets(originalButton.contentEdgeInsets, button.contentEdgeInsets))
{
- return @"···";
+ // no need to replace, it's the same
+ return;
}
+ NSMutableArray *items = [self.navigationItem.rightBarButtonItems mutableCopy];
+ items[replaceIndex] = threadListBarButtonItem;
+ self.navigationItem.rightBarButtonItems = items;
}
#pragma mark - RoomContextualMenuViewControllerDelegate
@@ -7156,6 +7241,20 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[self notifyDelegateOnLeaveRoomIfNecessary];
}
+- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didReplaceRoomWithReplacementId:(NSString *)roomId
+{
+ if (self.delegate)
+ {
+ [self.delegate roomViewController:self didReplaceRoomWithReplacementId:roomId];
+ }
+ else
+ {
+ ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES stackAboveVisibleViews:NO];
+ RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:nil mxSession:self.mainSession presentationParameters:presentationParameters showSettingsInitially:YES];
+ [[AppDelegate theDelegate] showRoomWithParameters:parameters];
+ }
+}
+
#pragma mark - RemoveJitsiWidgetViewDelegate
- (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view
@@ -7286,4 +7385,51 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[self updateThreadListBarButtonBadgeWith:service];
}
+#pragma mark - RoomParticipantsInviteCoordinatorBridgePresenterDelegate
+
+- (void)roomParticipantsInviteCoordinatorBridgePresenterDidComplete:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ self.participantsInvitePresenter = nil;
+}
+
+- (void)roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ [self startActivityIndicator];
+}
+
+- (void)roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ [self stopActivityIndicator];
+}
+
+#pragma mark - Live location sharing
+
+- (void)showLiveLocationBannerView
+{
+ if (self.liveLocationSharingBannerView)
+ {
+ return;
+ }
+
+ LiveLocationSharingBannerView *bannerView = [LiveLocationSharingBannerView instantiate];
+
+ [bannerView updateWithTheme:ThemeService.shared.theme];
+
+ MXWeakify(self);
+
+ bannerView.didTapBackground = ^{
+ MXStrongifyAndReturnIfNil(self);
+ [self.delegate roomViewControllerDidTapLiveLocationSharingBanner:self];
+ };
+
+ bannerView.didTapStopButton = ^{
+ MXStrongifyAndReturnIfNil(self);
+ [self.delegate roomViewControllerDidStopLiveLocationSharing:self];
+ };
+
+ [self.topBannersStackView addArrangedSubview:bannerView];
+
+ self.liveLocationSharingBannerView = bannerView;
+}
+
@end
diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib
index e8b8bfd6e..783146c29 100644
--- a/Riot/Modules/Room/RoomViewController.xib
+++ b/Riot/Modules/Room/RoomViewController.xib
@@ -13,7 +13,6 @@
-
@@ -32,6 +31,7 @@
+
@@ -41,8 +41,25 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -192,6 +209,7 @@
+
@@ -200,17 +218,19 @@
+
+
-
+
diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m
index eba0ee508..235d89798 100644
--- a/Riot/Modules/Room/Search/RoomSearchViewController.m
+++ b/Riot/Modules/Room/Search/RoomSearchViewController.m
@@ -164,6 +164,7 @@
mxSession:self.mainSession
threadParameters:threadParameters
presentationParameters:screenParameters];
+ Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomSearch;
[[LegacyAppDelegate theDelegate] showRoomWithParameters:parameters];
}
diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.h b/Riot/Modules/Room/Settings/RoomSettingsViewController.h
index 02ec6a815..5def882df 100644
--- a/Riot/Modules/Room/Settings/RoomSettingsViewController.h
+++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.h
@@ -20,6 +20,7 @@
#import "TableViewCellWithCheckBoxes.h"
@class AnalyticsScreenTracker;
+@protocol RoomSettingsViewControllerDelegate;
/**
List the settings fields. Used to preselect/edit a field
@@ -59,5 +60,21 @@ typedef enum : NSUInteger {
*/
@property (nonatomic) AnalyticsScreenTracker *screenTracker;
+/**
+ Delegate of this view controller.
+ */
+@property (nonatomic, weak) id delegate;
+
@end
+@protocol RoomSettingsViewControllerDelegate
+
+- (void)roomSettingsViewControllerDidLeaveRoom:(RoomSettingsViewController *)controller;
+
+- (void)roomSettingsViewController:(RoomSettingsViewController *)controller didReplaceRoomWithReplacementId:(NSString *)newRoomId;
+
+- (void)roomSettingsViewControllerDidCancel:(RoomSettingsViewController *)controller;
+
+- (void)roomSettingsViewControllerDidComplete:(RoomSettingsViewController *)controller;
+
+@end
diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m
index b1743d734..cd7295c22 100644
--- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m
+++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m
@@ -38,6 +38,7 @@ enum
{
SECTION_TAG_MAIN,
SECTION_TAG_ACCESS,
+ SECTION_TAG_PROMOTION,
SECTION_TAG_HISTORY,
SECTION_TAG_ADDRESSES,
SECTION_TAG_FLAIR,
@@ -58,6 +59,7 @@ enum
enum
{
+ ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ACCESS,
ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_INVITED_ONLY,
ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ANYONE_APART_FROM_GUEST,
ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ANYONE,
@@ -65,6 +67,11 @@ enum
ROOM_SETTINGS_ROOM_ACCESS_MISSING_ADDRESS_WARNING
};
+enum
+{
+ ROOM_SETTINGS_ROOM_PROMOTE_SECTION_ROW_SUGGEST
+};
+
enum
{
ROOM_SETTINGS_HISTORY_VISIBILITY_SECTION_ROW_ANYONE,
@@ -126,7 +133,7 @@ NSString *const kRoomSettingsAdvancedCellViewIdentifier = @"kRoomSettingsAdvance
NSString *const kRoomSettingsAdvancedEnableE2eCellViewIdentifier = @"kRoomSettingsAdvancedEnableE2eCellViewIdentifier";
NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSettingsAdvancedE2eEnabledCellViewIdentifier";
-@interface RoomSettingsViewController ()
+@interface RoomSettingsViewController ()
{
// The updated user data
NSMutableDictionary *updatedItemsDict;
@@ -178,6 +185,10 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
id kThemeServiceDidChangeThemeNotificationObserver;
+
+ RoomAccessCoordinatorBridgePresenter *roomAccessPresenter;
+
+ RoomSuggestionCoordinatorBridgePresenter *roomSuggestionPresenter;
}
@property (nonatomic, strong) SingleImagePickerPresenter *imagePickerPresenter;
@@ -261,6 +272,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
[self.tableView registerClass:TableViewCellWithCheckBoxes.class forCellReuseIdentifier:[TableViewCellWithCheckBoxes defaultReuseIdentifier]];
[self.tableView registerClass:TableViewCellWithCheckBoxAndLabel.class forCellReuseIdentifier:[TableViewCellWithCheckBoxAndLabel defaultReuseIdentifier]];
[self.tableView registerClass:MXKTableViewCell.class forCellReuseIdentifier:[MXKTableViewCell defaultReuseIdentifier]];
+ [self.tableView registerClass:TitleAndRightDetailTableViewCell.class forCellReuseIdentifier:[TitleAndRightDetailTableViewCell defaultReuseIdentifier]];
// Enable self sizing cells
self.tableView.rowHeight = UITableViewAutomaticDimension;
@@ -534,24 +546,45 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
if (RiotSettings.shared.roomSettingsScreenAllowChangingAccessSettings)
{
Section *sectionAccess = [Section sectionWithTag:SECTION_TAG_ACCESS];
- [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_INVITED_ONLY];
- [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ANYONE_APART_FROM_GUEST];
- [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ANYONE];
-
- // Check whether a room address is required for the current join rule
- NSString *joinRule = updatedItemsDict[kRoomSettingsJoinRuleKey];
- if (!joinRule)
+ if (@available(iOS 14, *))
{
- // Use the actual values if no change is pending.
- joinRule = mxRoomState.joinRule;
+ [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ACCESS];
+
+ // Check whether a room address is required for the current join rule
+ NSString *joinRule = updatedItemsDict[kRoomSettingsJoinRuleKey];
+ if (!joinRule)
+ {
+ // Use the actual values if no change is pending.
+ joinRule = mxRoomState.joinRule;
+ }
+
+ if ([joinRule isEqualToString:kMXRoomJoinRulePublic] && !roomAddresses.count)
+ {
+ // Notify the user that a room address is required.
+ [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_MISSING_ADDRESS_WARNING];
+ }
}
-
- if ([joinRule isEqualToString:kMXRoomJoinRulePublic] && !roomAddresses.count)
+ else
{
- // Notify the user that a room address is required.
- [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_MISSING_ADDRESS_WARNING];
+ [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_INVITED_ONLY];
+ [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ANYONE_APART_FROM_GUEST];
+ [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ANYONE];
+
+ // Check whether a room address is required for the current join rule
+ NSString *joinRule = updatedItemsDict[kRoomSettingsJoinRuleKey];
+ if (!joinRule)
+ {
+ // Use the actual values if no change is pending.
+ joinRule = mxRoomState.joinRule;
+ }
+
+ if ([joinRule isEqualToString:kMXRoomJoinRulePublic] && !roomAddresses.count)
+ {
+ // Notify the user that a room address is required.
+ [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_MISSING_ADDRESS_WARNING];
+ }
+ [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_DIRECTORY_VISIBILITY];
}
- [sectionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_DIRECTORY_VISIBILITY];
if (mxRoom.isDirect)
{
@@ -562,6 +595,17 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
sectionAccess.headerTitle = [VectorL10n roomDetailsAccessSection];
}
[tmpSections addObject:sectionAccess];
+
+ if (@available(iOS 14, *)) {
+ if (RiotSettings.shared.roomSettingsScreenAllowChangingAccessSettings)
+ {
+ Section *promotionAccess = [Section sectionWithTag:SECTION_TAG_PROMOTION];
+ promotionAccess.headerTitle = VectorL10n.roomDetailsPromoteRoomTitle;
+ [promotionAccess addRowWithTag:ROOM_SETTINGS_ROOM_ACCESS_DIRECTORY_VISIBILITY];
+ [promotionAccess addRowWithTag:ROOM_SETTINGS_ROOM_PROMOTE_SECTION_ROW_SUGGEST];
+ [tmpSections addObject:promotionAccess];
+ }
+ }
}
if (RiotSettings.shared.roomSettingsScreenAllowChangingHistorySettings)
@@ -856,7 +900,14 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
[self->updatedItemsDict removeAllObjects];
- [self withdrawViewControllerAnimated:YES completion:nil];
+ if (self.delegate)
+ {
+ [self.delegate roomSettingsViewControllerDidCancel:self];
+ }
+ else
+ {
+ [self withdrawViewControllerAnimated:YES completion:nil];
+ }
}
}]];
@@ -1385,7 +1436,14 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
}
else
{
- [self withdrawViewControllerAnimated:YES completion:nil];
+ if (self.delegate)
+ {
+ [self.delegate roomSettingsViewControllerDidCancel:self];
+ }
+ else
+ {
+ [self withdrawViewControllerAnimated:YES completion:nil];
+ }
}
}
@@ -2133,7 +2191,14 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
[self stopActivityIndicator];
- [self withdrawViewControllerAnimated:YES completion:nil];
+ if (self.delegate)
+ {
+ [self.delegate roomSettingsViewControllerDidComplete:self];
+ }
+ else
+ {
+ [self withdrawViewControllerAnimated:YES completion:nil];
+ }
}
#pragma mark - UITableViewDataSource
@@ -2491,6 +2556,41 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = [VectorL10n roomDetailsAccessSectionNoAddressWarning];
}
+ else if (row == ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ACCESS)
+ {
+ TitleAndRightDetailTableViewCell *roomAccessCell = [tableView dequeueReusableCellWithIdentifier:[TitleAndRightDetailTableViewCell defaultReuseIdentifier] forIndexPath:indexPath];
+
+ // Retrieve the potential updated values for joinRule and guestAccess
+ NSString *joinRule = updatedItemsDict[kRoomSettingsJoinRuleKey];
+ NSString *guestAccess = updatedItemsDict[kRoomSettingsGuestAccessKey];
+
+ // Use the actual values if no change is pending
+ if (!joinRule)
+ {
+ joinRule = mxRoomState.joinRule;
+ }
+ if (!guestAccess)
+ {
+ guestAccess = mxRoomState.guestAccess;
+ }
+
+ roomAccessCell.titleLabel.text = [VectorL10n roomDetailsAccessRowTitle];
+ NSString *access = VectorL10n.private;
+ if ([joinRule isEqualToString:kMXRoomJoinRulePublic])
+ {
+ access = VectorL10n.public;
+ }
+ else if ([joinRule isEqualToString:kMXRoomJoinRuleRestricted])
+ {
+ access = VectorL10n.createRoomTypeRestricted;
+ }
+ roomAccessCell.detailLabel.text = access;
+
+ // Check whether the user can change this option
+ roomAccessCell.userInteractionEnabled = (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomJoinRules]);
+
+ cell = roomAccessCell;
+ }
else
{
TableViewCellWithCheckBoxAndLabel *roomAccessCell = [tableView dequeueReusableCellWithIdentifier:[TableViewCellWithCheckBoxAndLabel defaultReuseIdentifier] forIndexPath:indexPath];
@@ -2557,6 +2657,54 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
cell = roomAccessCell;
}
}
+ else if (section == SECTION_TAG_PROMOTION)
+ {
+ if (row == ROOM_SETTINGS_ROOM_ACCESS_DIRECTORY_VISIBILITY)
+ {
+ MXKTableViewCellWithLabelAndSwitch *directoryToggleCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
+
+ if (mxRoom.isDirect)
+ {
+ directoryToggleCell.mxkLabel.text = [VectorL10n roomDetailsAccessSectionDirectoryToggleForDm];
+ }
+ else
+ {
+ directoryToggleCell.mxkLabel.text = [VectorL10n roomDetailsAccessSectionDirectoryToggle];
+ }
+
+ [directoryToggleCell.mxkSwitch addTarget:self action:@selector(toggleDirectoryVisibility:) forControlEvents:UIControlEventValueChanged];
+
+ if (updatedItemsDict[kRoomSettingsDirectoryKey])
+ {
+ directoryToggleCell.mxkSwitch.on = ((NSNumber*) updatedItemsDict[kRoomSettingsDirectoryKey]).boolValue;
+ }
+ else
+ {
+ // Use the last retrieved value if any
+ directoryToggleCell.mxkSwitch.on = actualDirectoryVisibility ? [actualDirectoryVisibility isEqualToString:kMXRoomDirectoryVisibilityPublic] : NO;
+ }
+
+ // Check whether the user can change this option
+ directoryToggleCell.mxkSwitch.enabled = (oneSelfPowerLevel >= powerLevels.stateDefault);
+
+ // Store the switch to be able to update it
+ directoryVisibilitySwitch = directoryToggleCell.mxkSwitch;
+
+ cell = directoryToggleCell;
+ }
+ else if (row == ROOM_SETTINGS_ROOM_PROMOTE_SECTION_ROW_SUGGEST)
+ {
+ TitleAndRightDetailTableViewCell *roomSuggestionCell = [tableView dequeueReusableCellWithIdentifier:[TitleAndRightDetailTableViewCell defaultReuseIdentifier] forIndexPath:indexPath];
+
+ roomSuggestionCell.titleLabel.text = [VectorL10n roomDetailsPromoteRoomSuggestTitle];
+ roomSuggestionCell.detailLabel.text = [self.mainSession.spaceService directParentIdsOfRoomWithId:self.roomId whereRoomIsSuggested:YES].count ? [VectorL10n on] : [VectorL10n off];
+
+ // Check whether the user can change this option
+ roomSuggestionCell.userInteractionEnabled = (oneSelfPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomJoinRules]);
+
+ cell = roomSuggestionCell;
+ }
+ }
else if (section == SECTION_TAG_HISTORY)
{
TableViewCellWithCheckBoxAndLabel *historyVisibilityCell = [tableView dequeueReusableCellWithIdentifier:[TableViewCellWithCheckBoxAndLabel defaultReuseIdentifier] forIndexPath:indexPath];
@@ -3141,6 +3289,10 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
}
}
}
+ else if (row == ROOM_SETTINGS_ROOM_ACCESS_SECTION_ROW_ACCESS)
+ {
+ [self showRoomAccessFlow];
+ }
if (isUpdated)
{
@@ -3149,6 +3301,13 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
[self getNavigationItem].rightBarButtonItem.enabled = (updatedItemsDict.count != 0);
}
}
+ else if (section == SECTION_TAG_PROMOTION)
+ {
+ if (row == ROOM_SETTINGS_ROOM_PROMOTE_SECTION_ROW_SUGGEST)
+ {
+ [self showSuggestToSpaceMembers];
+ }
+ }
else if (section == SECTION_TAG_HISTORY)
{
// Ignore the selection if the option is already enabled
@@ -3502,7 +3661,11 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
[self startActivityIndicator];
[self->mxRoom leave:^{
- [[LegacyAppDelegate theDelegate] restoreInitialDisplay:nil];
+ if (self.delegate) {
+ [self.delegate roomSettingsViewControllerDidLeaveRoom:self];
+ } else {
+ [[LegacyAppDelegate theDelegate] restoreInitialDisplay:nil];
+ }
} failure:^(NSError *error) {
@@ -3667,6 +3830,28 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
[self getNavigationItem].rightBarButtonItem.enabled = (updatedItemsDict.count != 0);
}
+- (void)showRoomAccessFlow
+{
+ MXRoom *room = [self.mainSession roomWithRoomId:self.roomId];
+
+ if (room) {
+ roomAccessPresenter = [[RoomAccessCoordinatorBridgePresenter alloc] initWithRoom:room];
+ roomAccessPresenter.delegate = self;
+ [roomAccessPresenter presentFrom:self animated:YES];
+ }
+}
+
+- (void)showSuggestToSpaceMembers
+{
+ MXRoom *room = [self.mainSession roomWithRoomId:self.roomId];
+
+ if (room) {
+ roomSuggestionPresenter = [[RoomSuggestionCoordinatorBridgePresenter alloc] initWithRoom:room];
+ roomSuggestionPresenter.delegate = self;
+ [roomSuggestionPresenter presentFrom:self animated:YES];
+ }
+}
+
- (void)setRoomAliasAsMainAddress:(NSString *)alias
{
NSString *currentCanonicalAlias = mxRoomState.canonicalAlias;
@@ -4037,4 +4222,52 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
[self.tableView reloadData];
}
+#pragma mark - RoomAccessCoordinatorBridgePresenterDelegate
+
+- (void)roomAccessCoordinatorBridgePresenterDelegate:(RoomAccessCoordinatorBridgePresenter *)coordinatorBridgePresenter didCancelRoomWithId:(NSString *)roomId
+{
+ if (![roomId isEqualToString: self.roomId]) {
+ // Room Access Coordinator upgraded the actual room -> Need to move to replacement room
+ [self.delegate roomSettingsViewController:self didReplaceRoomWithReplacementId:roomId];
+ }
+
+ MXWeakify(self);
+ [roomAccessPresenter dismissWithAnimated:YES completion:^{
+ MXStrongifyAndReturnIfNil(self);
+ self->roomAccessPresenter = nil;
+ }];
+}
+
+- (void)roomAccessCoordinatorBridgePresenterDelegate:(RoomAccessCoordinatorBridgePresenter *)coordinatorBridgePresenter didCompleteRoomWithId:(NSString *)roomId
+{
+ if (![roomId isEqualToString: self.roomId]) {
+ // Room Access Coordinator upgraded the actual room -> Need to move to replacement room
+ [self.delegate roomSettingsViewController:self didReplaceRoomWithReplacementId:roomId];
+ }
+
+ MXWeakify(self);
+ [roomAccessPresenter dismissWithAnimated:YES completion:^{
+ MXStrongifyAndReturnIfNil(self);
+ self->roomAccessPresenter = nil;
+ }];
+}
+
+#pragma mark - RoomSuggestionCoordinatorBridgePresenterDelegate
+
+- (void)roomSuggestionCoordinatorBridgePresenterDelegateDidCancel:(RoomSuggestionCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ [roomSuggestionPresenter dismissWithAnimated:YES completion:nil];
+ roomSuggestionPresenter = nil;
+}
+
+- (void)roomSuggestionCoordinatorBridgePresenterDelegateDidComplete:(RoomSuggestionCoordinatorBridgePresenter *)coordinatorBridgePresenter
+{
+ MXWeakify(self);
+ [roomSuggestionPresenter dismissWithAnimated:YES completion:^{
+ MXStrongifyAndReturnIfNil(self);
+ self->roomSuggestionPresenter = nil;
+ [self refreshRoomSettings];
+ }];
+}
+
@end
diff --git a/Riot/Modules/Room/Settings/Views/TitleAndRightDetailTableViewCell.swift b/Riot/Modules/Room/Settings/Views/TitleAndRightDetailTableViewCell.swift
new file mode 100644
index 000000000..39410d86a
--- /dev/null
+++ b/Riot/Modules/Room/Settings/Views/TitleAndRightDetailTableViewCell.swift
@@ -0,0 +1,48 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// 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 UIKit
+
+class TitleAndRightDetailTableViewCell: MXKTableViewCell {
+
+ // MARK: Outlet
+
+ @IBOutlet var titleLabel: UILabel!
+ @IBOutlet var detailLabel: UILabel!
+
+ // MARK: Properties
+
+ override var isUserInteractionEnabled: Bool {
+ didSet {
+ titleLabel.alpha = isUserInteractionEnabled ? 1 : 0.3
+ detailLabel.alpha = isUserInteractionEnabled ? 1 : 0.3
+ }
+ }
+
+ // MARK: - MXKTableViewCell
+
+ override func customizeRendering() {
+ super.customizeRendering()
+
+ let theme = ThemeService.shared().theme
+
+ titleLabel.textColor = theme.colors.primaryContent
+ titleLabel.font = theme.fonts.body
+
+ detailLabel.textColor = theme.colors.secondaryContent
+ detailLabel.font = theme.fonts.body
+ }
+}
diff --git a/Riot/Modules/Room/Settings/Views/TitleAndRightDetailTableViewCell.xib b/Riot/Modules/Room/Settings/Views/TitleAndRightDetailTableViewCell.xib
new file mode 100644
index 000000000..4be3861c3
--- /dev/null
+++ b/Riot/Modules/Room/Settings/Views/TitleAndRightDetailTableViewCell.xib
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m
index 6e4e066d5..f7b54cf6a 100644
--- a/Riot/Modules/Rooms/RoomsViewController.m
+++ b/Riot/Modules/Rooms/RoomsViewController.m
@@ -110,9 +110,13 @@
[self.tableViewPaginationThrottler throttle:^{
NSInteger section = indexPath.section;
+ if (tableView.numberOfSections <= section)
+ {
+ return;
+ }
+
NSInteger numberOfRowsInSection = [tableView numberOfRowsInSection:section];
- if (tableView.numberOfSections > section
- && indexPath.row == numberOfRowsInSection - 1)
+ if (indexPath.row == numberOfRowsInSection - 1)
{
[self->recentsDataSource paginateInSection:section];
}
diff --git a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift
index c9be632dd..efb42a35a 100644
--- a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift
+++ b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift
@@ -24,6 +24,7 @@ final class SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyCoordinator
private let secretsRecoveryWithKeyViewController: SecretsRecoveryWithKeyViewController
private let secretsRecoveryWithKeyViewModel: SecretsRecoveryWithKeyViewModel
+ private let cancellable: Bool
// MARK: Public
@@ -33,12 +34,13 @@ final class SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyCoordinator
// MARK: - Setup
- init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal) {
+ init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal, cancellable: Bool) {
let secretsRecoveryWithKeyViewModel = SecretsRecoveryWithKeyViewModel(recoveryService: recoveryService, recoveryGoal: recoveryGoal)
- let secretsRecoveryWithKeyViewController = SecretsRecoveryWithKeyViewController.instantiate(with: secretsRecoveryWithKeyViewModel)
+ let secretsRecoveryWithKeyViewController = SecretsRecoveryWithKeyViewController.instantiate(with: secretsRecoveryWithKeyViewModel, cancellable: cancellable)
self.secretsRecoveryWithKeyViewController = secretsRecoveryWithKeyViewController
self.secretsRecoveryWithKeyViewModel = secretsRecoveryWithKeyViewModel
+ self.cancellable = cancellable
}
// MARK: - Public
@@ -49,6 +51,7 @@ final class SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyCoordinator
func toPresentable() -> UIViewController {
return self.secretsRecoveryWithKeyViewController
+ .vc_setModalFullScreen(!self.cancellable)
}
}
diff --git a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift
index 23b48397e..a746656a6 100644
--- a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift
+++ b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift
@@ -43,6 +43,7 @@ final class SecretsRecoveryWithKeyViewController: UIViewController {
private var viewModel: SecretsRecoveryWithKeyViewModelType!
private var keyboardAvoider: KeyboardAvoider?
+ private var cancellable: Bool!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
@@ -52,9 +53,10 @@ final class SecretsRecoveryWithKeyViewController: UIViewController {
// MARK: - Setup
- class func instantiate(with viewModel: SecretsRecoveryWithKeyViewModelType) -> SecretsRecoveryWithKeyViewController {
+ class func instantiate(with viewModel: SecretsRecoveryWithKeyViewModelType, cancellable: Bool) -> SecretsRecoveryWithKeyViewController {
let viewController = StoryboardScene.SecretsRecoveryWithKeyViewController.initialScene.instantiate()
viewController.viewModel = viewModel
+ viewController.cancellable = cancellable
viewController.theme = ThemeService.shared().theme
return viewController
}
@@ -86,11 +88,13 @@ final class SecretsRecoveryWithKeyViewController: UIViewController {
// MARK: - Private
private func setupViews() {
- let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
- self?.cancelButtonAction()
+ if self.cancellable {
+ let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
+ self?.cancelButtonAction()
+ }
+ self.navigationItem.rightBarButtonItem = cancelBarButtonItem
}
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
-
+
self.title = VectorL10n.secretsRecoveryWithKeyTitle
self.scrollView.keyboardDismissMode = .interactive
diff --git a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift
index bb2390050..02133fcbc 100644
--- a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift
+++ b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift
@@ -24,6 +24,7 @@ final class SecretsRecoveryWithPassphraseCoordinator: SecretsRecoveryWithPassphr
private let secretsRecoveryWithPassphraseViewController: SecretsRecoveryWithPassphraseViewController
private var secretsRecoveryWithPassphraseViewModel: SecretsRecoveryWithPassphraseViewModelType
+ private let cancellable: Bool
// MARK: Public
@@ -33,11 +34,12 @@ final class SecretsRecoveryWithPassphraseCoordinator: SecretsRecoveryWithPassphr
// MARK: - Setup
- init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal) {
+ init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal, cancellable: Bool) {
let secretsRecoveryWithPassphraseViewModel = SecretsRecoveryWithPassphraseViewModel(recoveryService: recoveryService, recoveryGoal: recoveryGoal)
- let secretsRecoveryWithPassphraseViewController = SecretsRecoveryWithPassphraseViewController.instantiate(with: secretsRecoveryWithPassphraseViewModel)
+ let secretsRecoveryWithPassphraseViewController = SecretsRecoveryWithPassphraseViewController.instantiate(with: secretsRecoveryWithPassphraseViewModel, cancellable: cancellable)
self.secretsRecoveryWithPassphraseViewController = secretsRecoveryWithPassphraseViewController
self.secretsRecoveryWithPassphraseViewModel = secretsRecoveryWithPassphraseViewModel
+ self.cancellable = cancellable
}
// MARK: - Public
@@ -48,6 +50,7 @@ final class SecretsRecoveryWithPassphraseCoordinator: SecretsRecoveryWithPassphr
func toPresentable() -> UIViewController {
return self.secretsRecoveryWithPassphraseViewController
+ .vc_setModalFullScreen(!self.cancellable)
}
}
diff --git a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift
index 0578f2b37..a29e304ae 100644
--- a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift
+++ b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift
@@ -44,6 +44,7 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController {
private var viewModel: SecretsRecoveryWithPassphraseViewModelType!
private var keyboardAvoider: KeyboardAvoider?
+ private var cancellable: Bool!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
@@ -52,9 +53,10 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController {
// MARK: - Setup
- class func instantiate(with viewModel: SecretsRecoveryWithPassphraseViewModelType) -> SecretsRecoveryWithPassphraseViewController {
+ class func instantiate(with viewModel: SecretsRecoveryWithPassphraseViewModelType, cancellable: Bool) -> SecretsRecoveryWithPassphraseViewController {
let viewController = StoryboardScene.SecretsRecoveryWithPassphraseViewController.initialScene.instantiate()
viewController.viewModel = viewModel
+ viewController.cancellable = cancellable
viewController.theme = ThemeService.shared().theme
return viewController
}
@@ -86,11 +88,13 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController {
// MARK: - Private
private func setupViews() {
- let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
- self?.viewModel.process(viewAction: .cancel)
+ if self.cancellable {
+ let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
+ self?.viewModel.process(viewAction: .cancel)
+ }
+ self.navigationItem.rightBarButtonItem = cancelBarButtonItem
}
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
-
+
self.title = VectorL10n.secretsRecoveryWithPassphraseTitle
self.scrollView.keyboardDismissMode = .interactive
diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift
index fdca1dc7d..d36ee995e 100644
--- a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift
+++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift
@@ -26,6 +26,7 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType {
private let navigationRouter: NavigationRouterType
private let recoveryMode: SecretsRecoveryMode
private let recoveryGoal: SecretsRecoveryGoal
+ private let cancellable: Bool
// MARK: Public
@@ -35,10 +36,11 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType {
// MARK: - Setup
- init(session: MXSession, recoveryMode: SecretsRecoveryMode, recoveryGoal: SecretsRecoveryGoal, navigationRouter: NavigationRouterType? = nil) {
+ init(session: MXSession, recoveryMode: SecretsRecoveryMode, recoveryGoal: SecretsRecoveryGoal, navigationRouter: NavigationRouterType? = nil, cancellable: Bool) {
self.session = session
self.recoveryMode = recoveryMode
self.recoveryGoal = recoveryGoal
+ self.cancellable = cancellable
if let navigationRouter = navigationRouter {
self.navigationRouter = navigationRouter
@@ -76,19 +78,21 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType {
}
func toPresentable() -> UIViewController {
- return self.navigationRouter.toPresentable()
+ return self.navigationRouter
+ .toPresentable()
+ .vc_setModalFullScreen(!self.cancellable)
}
// MARK: - Private
private func createRecoverFromKeyCoordinator() -> SecretsRecoveryWithKeyCoordinator {
- let coordinator = SecretsRecoveryWithKeyCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal)
+ let coordinator = SecretsRecoveryWithKeyCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal, cancellable: self.cancellable)
coordinator.delegate = self
return coordinator
}
private func createRecoverFromPassphraseCoordinator() -> SecretsRecoveryWithPassphraseCoordinator {
- let coordinator = SecretsRecoveryWithPassphraseCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal)
+ let coordinator = SecretsRecoveryWithPassphraseCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal, cancellable: self.cancellable)
coordinator.delegate = self
return coordinator
}
@@ -115,7 +119,7 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType {
}
private func showSecureBackupSetup(checkKeyBackup: Bool) {
- let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter)
+ let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter, cancellable: self.cancellable)
coordinator.delegate = self
coordinator.start()
diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift
index a5a1ea31a..3011cfe3d 100644
--- a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift
+++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift
@@ -74,7 +74,7 @@ final class SecretsRecoveryCoordinatorBridgePresenter: NSObject {
func present(from viewController: UIViewController, animated: Bool) {
- let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: self.recoveryMode, recoveryGoal: self.recoveryGoal.goal)
+ let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: self.recoveryMode, recoveryGoal: self.recoveryGoal.goal, cancellable: true)
coordinator.delegate = self
let presentable = coordinator.toPresentable()
diff --git a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift
index f08f88869..58ebf79ba 100644
--- a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift
+++ b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift
@@ -27,6 +27,7 @@ final class SecretsSetupRecoveryKeyCoordinator: SecretsSetupRecoveryKeyCoordinat
private var secretsSetupRecoveryKeyViewModel: SecretsSetupRecoveryKeyViewModelType
private let secretsSetupRecoveryKeyViewController: SecretsSetupRecoveryKeyViewController
+ private let cancellable: Bool
// MARK: Public
@@ -40,11 +41,13 @@ final class SecretsSetupRecoveryKeyCoordinator: SecretsSetupRecoveryKeyCoordinat
init(recoveryService: MXRecoveryService,
passphrase: String?,
passphraseOnly: Bool,
- allowOverwrite: Bool = false) {
+ allowOverwrite: Bool = false,
+ cancellable: Bool) {
let secretsSetupRecoveryKeyViewModel = SecretsSetupRecoveryKeyViewModel(recoveryService: recoveryService, passphrase: passphrase, passphraseOnly: passphraseOnly, allowOverwrite: allowOverwrite)
- let secretsSetupRecoveryKeyViewController = SecretsSetupRecoveryKeyViewController.instantiate(with: secretsSetupRecoveryKeyViewModel)
+ let secretsSetupRecoveryKeyViewController = SecretsSetupRecoveryKeyViewController.instantiate(with: secretsSetupRecoveryKeyViewModel, cancellable: cancellable)
self.secretsSetupRecoveryKeyViewModel = secretsSetupRecoveryKeyViewModel
self.secretsSetupRecoveryKeyViewController = secretsSetupRecoveryKeyViewController
+ self.cancellable = cancellable
}
// MARK: - Public methods
@@ -55,6 +58,7 @@ final class SecretsSetupRecoveryKeyCoordinator: SecretsSetupRecoveryKeyCoordinat
func toPresentable() -> UIViewController {
return self.secretsSetupRecoveryKeyViewController
+ .vc_setModalFullScreen(!self.cancellable)
}
}
diff --git a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift
index e6be0650d..ea623fdfe 100644
--- a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift
+++ b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift
@@ -34,6 +34,7 @@ final class SecretsSetupRecoveryKeyViewController: UIViewController {
private var viewModel: SecretsSetupRecoveryKeyViewModelType!
private var isPassphraseOnly: Bool = true
+ private var cancellable: Bool!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
@@ -43,9 +44,10 @@ final class SecretsSetupRecoveryKeyViewController: UIViewController {
// MARK: - Setup
- class func instantiate(with viewModel: SecretsSetupRecoveryKeyViewModelType) -> SecretsSetupRecoveryKeyViewController {
+ class func instantiate(with viewModel: SecretsSetupRecoveryKeyViewModelType, cancellable: Bool) -> SecretsSetupRecoveryKeyViewController {
let viewController = StoryboardScene.SecretsSetupRecoveryKeyViewController.initialScene.instantiate()
viewController.viewModel = viewModel
+ viewController.cancellable = cancellable
viewController.theme = ThemeService.shared().theme
return viewController
}
@@ -108,12 +110,14 @@ final class SecretsSetupRecoveryKeyViewController: UIViewController {
}
private func setupViews() {
- let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
- self?.cancelButtonAction()
+ if self.cancellable {
+ let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
+ self?.cancelButtonAction()
+ }
+
+ self.navigationItem.rightBarButtonItem = cancelBarButtonItem
}
-
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
-
+
self.vc_removeBackTitle()
self.title = VectorL10n.secretsSetupRecoveryKeyTitle
diff --git a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift
index 2f67abb74..a8154facc 100644
--- a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift
+++ b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift
@@ -27,6 +27,7 @@ final class SecretsSetupRecoveryPassphraseCoordinator: SecretsSetupRecoveryPassp
private var secretsSetupRecoveryPassphraseViewModel: SecretsSetupRecoveryPassphraseViewModelType
private let secretsSetupRecoveryPassphraseViewController: SecretsSetupRecoveryPassphraseViewController
+ private let cancellable: Bool
// MARK: Public
@@ -37,12 +38,13 @@ final class SecretsSetupRecoveryPassphraseCoordinator: SecretsSetupRecoveryPassp
// MARK: - Setup
- init(passphraseInput: SecretsSetupRecoveryPassphraseInput) {
+ init(passphraseInput: SecretsSetupRecoveryPassphraseInput, cancellable: Bool) {
let secretsSetupRecoveryPassphraseViewModel = SecretsSetupRecoveryPassphraseViewModel(passphraseInput: passphraseInput)
- let secretsSetupRecoveryPassphraseViewController = SecretsSetupRecoveryPassphraseViewController.instantiate(with: secretsSetupRecoveryPassphraseViewModel)
+ let secretsSetupRecoveryPassphraseViewController = SecretsSetupRecoveryPassphraseViewController.instantiate(with: secretsSetupRecoveryPassphraseViewModel, cancellable: cancellable)
self.secretsSetupRecoveryPassphraseViewModel = secretsSetupRecoveryPassphraseViewModel
self.secretsSetupRecoveryPassphraseViewController = secretsSetupRecoveryPassphraseViewController
+ self.cancellable = cancellable
}
// MARK: - Public methods
@@ -53,6 +55,7 @@ final class SecretsSetupRecoveryPassphraseCoordinator: SecretsSetupRecoveryPassp
func toPresentable() -> UIViewController {
return self.secretsSetupRecoveryPassphraseViewController
+ .vc_setModalFullScreen(!self.cancellable)
}
}
diff --git a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift
index 0cb5bd2d2..5ac8c91ea 100644
--- a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift
+++ b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift
@@ -53,6 +53,7 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController {
// MARK: Private
private var viewModel: SecretsSetupRecoveryPassphraseViewModelType!
+ private var cancellable: Bool!
private var theme: Theme!
private var keyboardAvoider: KeyboardAvoider?
private var errorPresenter: MXKErrorPresentation!
@@ -64,9 +65,10 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController {
// MARK: - Setup
- class func instantiate(with viewModel: SecretsSetupRecoveryPassphraseViewModelType) -> SecretsSetupRecoveryPassphraseViewController {
+ class func instantiate(with viewModel: SecretsSetupRecoveryPassphraseViewModelType, cancellable: Bool) -> SecretsSetupRecoveryPassphraseViewController {
let viewController = StoryboardScene.SecretsSetupRecoveryPassphraseViewController.initialScene.instantiate()
viewController.viewModel = viewModel
+ viewController.cancellable = cancellable
viewController.theme = ThemeService.shared().theme
return viewController
}
@@ -119,12 +121,14 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController {
// MARK: - Private
private func setupViews() {
- let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
- self?.cancelButtonAction()
+ if self.cancellable {
+ let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
+ self?.cancelButtonAction()
+ }
+
+ self.navigationItem.rightBarButtonItem = cancelBarButtonItem
}
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
-
self.vc_removeBackTitle()
self.title = VectorL10n.secretsSetupRecoveryPassphraseTitle
diff --git a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift
index 346f0c535..4af46f86f 100644
--- a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift
+++ b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift
@@ -39,6 +39,7 @@ final class SecureBackupSetupIntroViewController: UIViewController {
// MARK: Private
private var viewModel: SecureBackupSetupIntroViewModelType!
+ private var cancellable: Bool!
private var theme: Theme!
private var activityIndicatorPresenter: ActivityIndicatorPresenter!
@@ -50,9 +51,10 @@ final class SecureBackupSetupIntroViewController: UIViewController {
// MARK: - Setup
- class func instantiate(with viewModel: SecureBackupSetupIntroViewModelType) -> SecureBackupSetupIntroViewController {
+ class func instantiate(with viewModel: SecureBackupSetupIntroViewModelType, cancellable: Bool) -> SecureBackupSetupIntroViewController {
let viewController = StoryboardScene.SecureBackupSetupIntroViewController.initialScene.instantiate()
viewController.viewModel = viewModel
+ viewController.cancellable = cancellable
viewController.theme = ThemeService.shared().theme
return viewController
}
@@ -86,13 +88,15 @@ final class SecureBackupSetupIntroViewController: UIViewController {
// MARK: - Private
private func setupViews() {
- let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
- guard let self = self else {
- return
+ if self.cancellable {
+ let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
+ guard let self = self else {
+ return
+ }
+ self.delegate?.secureBackupSetupIntroViewControllerDidCancel(self, showSkipAlert: true)
}
- self.delegate?.secureBackupSetupIntroViewControllerDidCancel(self, showSkipAlert: true)
+ self.navigationItem.rightBarButtonItem = cancelBarButtonItem
}
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
self.title = VectorL10n.secureKeyBackupSetupIntroTitle
@@ -119,6 +123,21 @@ final class SecureBackupSetupIntroViewController: UIViewController {
}
self.delegate?.secureBackupSetupIntroViewControllerDidTapUsePassphrase(self)
}
+
+ setupBackupMethods()
+ }
+
+ private func setupBackupMethods() {
+ let secureBackupSetupMethods = self.viewModel.homeserverEncryptionConfiguration.secureBackupSetupMethods
+
+ // Hide setup methods that are not listed
+ if !secureBackupSetupMethods.contains(.key) {
+ self.secureKeyCell.isHidden = true
+ }
+
+ if !secureBackupSetupMethods.contains(.passphrase) {
+ self.securePassphraseCell.isHidden = true
+ }
}
private func renderLoading() {
diff --git a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift
index 3a10148d9..e85653ee7 100644
--- a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift
+++ b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift
@@ -23,11 +23,13 @@ final class SecureBackupSetupIntroViewModel: SecureBackupSetupIntroViewModelType
// TODO: Make these properties private
let keyBackup: MXKeyBackup?
let checkKeyBackup: Bool
+ let homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration
// MARK: - Setup
- init(keyBackup: MXKeyBackup?, checkKeyBackup: Bool) {
+ init(keyBackup: MXKeyBackup?, checkKeyBackup: Bool, homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration) {
self.keyBackup = keyBackup
self.checkKeyBackup = checkKeyBackup
- }
+ self.homeserverEncryptionConfiguration = homeserverEncryptionConfiguration
+ }
}
diff --git a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift
index b7d08f5d5..3e301fb56 100644
--- a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift
+++ b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift
@@ -22,4 +22,5 @@ protocol SecureBackupSetupIntroViewModelType {
// TODO: Hide these properties from interface and use same behavior as other view models
var keyBackup: MXKeyBackup? { get }
var checkKeyBackup: Bool { get }
+ var homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration { get }
}
diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift
index 543616377..3385063ce 100644
--- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift
+++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift
@@ -30,7 +30,14 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
private let recoveryService: MXRecoveryService
private let keyBackup: MXKeyBackup?
private let checkKeyBackup: Bool
+ private let homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration
private let allowOverwrite: Bool
+ private let cancellable: Bool
+
+ private var isBackupSetupMethodKeySupported: Bool {
+ let homeserverEncryptionConfiguration = self.session.vc_homeserverConfiguration().encryption
+ return homeserverEncryptionConfiguration.secureBackupSetupMethods.contains(.key)
+ }
// MARK: Public
@@ -46,12 +53,15 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
/// - session: The MXSession.
/// - checkKeyBackup: Indicate false to ignore existing key backup.
/// - navigationRouter: Use existing navigation router to plug this flow or let nil to use new one.
- init(session: MXSession, checkKeyBackup: Bool = true, allowOverwrite: Bool = false, navigationRouter: NavigationRouterType? = nil) {
+ /// - cancellable: Whether secure backup can be cancelled
+ init(session: MXSession, checkKeyBackup: Bool = true, allowOverwrite: Bool = false, navigationRouter: NavigationRouterType? = nil, cancellable: Bool) {
self.session = session
self.recoveryService = session.crypto.recoveryService
self.keyBackup = session.crypto.backup
self.checkKeyBackup = checkKeyBackup
+ self.homeserverEncryptionConfiguration = session.vc_homeserverConfiguration().encryption
self.allowOverwrite = allowOverwrite
+ self.cancellable = cancellable
if let navigationRouter = navigationRouter {
self.navigationRouter = navigationRouter
@@ -73,21 +83,25 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
}
func toPresentable() -> UIViewController {
- return self.navigationRouter.toPresentable()
+ return self.navigationRouter
+ .toPresentable()
+ .vc_setModalFullScreen(!self.cancellable)
}
// MARK: - Private methods
private func createIntro() -> SecureBackupSetupIntroViewController {
// TODO: Use a coordinator
- let viewModel = SecureBackupSetupIntroViewModel(keyBackup: self.keyBackup, checkKeyBackup: self.checkKeyBackup)
- let introViewController = SecureBackupSetupIntroViewController.instantiate(with: viewModel)
+ let viewModel = SecureBackupSetupIntroViewModel(keyBackup: self.keyBackup,
+ checkKeyBackup: self.checkKeyBackup,
+ homeserverEncryptionConfiguration: self.homeserverEncryptionConfiguration)
+ let introViewController = SecureBackupSetupIntroViewController.instantiate(with: viewModel, cancellable: self.cancellable)
introViewController.delegate = self
return introViewController
}
private func showSetupKey(passphraseOnly: Bool, passphrase: String? = nil) {
- let coordinator = SecretsSetupRecoveryKeyCoordinator(recoveryService: self.recoveryService, passphrase: passphrase, passphraseOnly: passphraseOnly, allowOverwrite: allowOverwrite)
+ let coordinator = SecretsSetupRecoveryKeyCoordinator(recoveryService: self.recoveryService, passphrase: passphrase, passphraseOnly: passphraseOnly, allowOverwrite: allowOverwrite, cancellable: self.cancellable)
coordinator.delegate = self
coordinator.start()
@@ -98,7 +112,7 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
}
private func showSetupPassphrase() {
- let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .new)
+ let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .new, cancellable: self.cancellable)
coordinator.delegate = self
coordinator.start()
@@ -109,7 +123,7 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
}
private func showSetupPassphraseConfirmation(with passphrase: String) {
- let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .confirm(passphrase))
+ let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .confirm(passphrase), cancellable: self.cancellable)
coordinator.delegate = self
coordinator.start()
@@ -203,7 +217,9 @@ extension SecureBackupSetupCoordinator: SecretsSetupRecoveryPassphraseCoordinato
}
func secretsSetupRecoveryPassphraseCoordinator(_ coordinator: SecretsSetupRecoveryPassphraseCoordinatorType, didConfirmPassphrase passphrase: String) {
- self.showSetupKey(passphraseOnly: false, passphrase: passphrase)
+
+ // Do not present recovery key export screen if secure backup setup key method is not supported
+ self.showSetupKey(passphraseOnly: !self.isBackupSetupMethodKeySupported, passphrase: passphrase)
}
func secretsSetupRecoveryPassphraseCoordinatorDidCancel(_ coordinator: SecretsSetupRecoveryPassphraseCoordinatorType) {
diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift
index 3db379e8f..60128a8a8 100644
--- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift
+++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift
@@ -37,9 +37,12 @@ final class SecureBackupSetupCoordinatorBridgePresenter: NSObject {
private var coordinator: SecureBackupSetupCoordinator?
// MARK: Public
-
weak var delegate: SecureBackupSetupCoordinatorBridgePresenterDelegate?
-
+
+ var isPresenting: Bool {
+ return self.coordinator != nil
+ }
+
// MARK: - Setup
init(session: MXSession, allowOverwrite: Bool) {
@@ -54,9 +57,13 @@ final class SecureBackupSetupCoordinatorBridgePresenter: NSObject {
// func present(from viewController: UIViewController, animated: Bool) {
// self.present(from: viewController, animated: animated)
// }
-
+
func present(from viewController: UIViewController, animated: Bool) {
- let secureBackupSetupCoordinator = SecureBackupSetupCoordinator(session: self.session, allowOverwrite: self.allowOverwrite)
+ self.present(from: viewController, animated: animated, cancellable: true)
+ }
+
+ func present(from viewController: UIViewController, animated: Bool, cancellable: Bool) {
+ let secureBackupSetupCoordinator = SecureBackupSetupCoordinator(session: self.session, allowOverwrite: self.allowOverwrite, cancellable: cancellable)
secureBackupSetupCoordinator.delegate = self
viewController.present(secureBackupSetupCoordinator.toPresentable(), animated: animated, completion: nil)
secureBackupSetupCoordinator.start()
diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m
index 280413786..3e47ba057 100644
--- a/Riot/Modules/Settings/Security/SecurityViewController.m
+++ b/Riot/Modules/Settings/Security/SecurityViewController.m
@@ -287,6 +287,8 @@ TableViewSectionsDelegate>
- (void)updateSections
{
NSMutableArray *sections = [NSMutableArray array];
+
+ BOOL isSecureBackupRequired = self.mainSession.vc_homeserverConfiguration.encryption.isSecureBackupRequired;
// Pin code section
@@ -341,14 +343,17 @@ TableViewSectionsDelegate>
}
// Secure backup
-
- Section *secureBackupSection = [Section sectionWithTag:SECTION_SECURE_BACKUP];
- secureBackupSection.headerTitle = [VectorL10n securitySettingsSecureBackup];
- secureBackupSection.footerTitle = VectorL10n.securitySettingsSecureBackupDescription;
-
- [secureBackupSection addRowsWithCount:self->secureBackupSection.numberOfRows];
-
- [sections addObject:secureBackupSection];
+
+ if (!isSecureBackupRequired)
+ {
+ Section *secureBackupSection = [Section sectionWithTag:SECTION_SECURE_BACKUP];
+ secureBackupSection.headerTitle = [VectorL10n securitySettingsSecureBackup];
+ secureBackupSection.footerTitle = VectorL10n.securitySettingsSecureBackupDescription;
+
+ [secureBackupSection addRowsWithCount:self->secureBackupSection.numberOfRows];
+
+ [sections addObject:secureBackupSection];
+ }
// Cross-Signing
@@ -359,24 +364,24 @@ TableViewSectionsDelegate>
[sections addObject:crossSigningSection];
- // Cryptograhpy
+ // Cryptography
- Section *cryptograhpySection = [Section sectionWithTag:SECTION_CRYPTOGRAPHY];
- cryptograhpySection.headerTitle = [VectorL10n securitySettingsCryptography];
+ Section *cryptographySection = [Section sectionWithTag:SECTION_CRYPTOGRAPHY];
+ cryptographySection.headerTitle = [VectorL10n securitySettingsCryptography];
if (RiotSettings.shared.settingsSecurityScreenShowCryptographyInfo)
{
- [cryptograhpySection addRowWithTag:CRYPTOGRAPHY_INFO];
+ [cryptographySection addRowWithTag:CRYPTOGRAPHY_INFO];
}
- if (RiotSettings.shared.settingsSecurityScreenShowCryptographyExport)
+ if (RiotSettings.shared.settingsSecurityScreenShowCryptographyExport && !isSecureBackupRequired)
{
- [cryptograhpySection addRowWithTag:CRYPTOGRAPHY_EXPORT];
+ [cryptographySection addRowWithTag:CRYPTOGRAPHY_EXPORT];
}
- if (cryptograhpySection.rows.count)
+ if (cryptographySection.rows.count)
{
- [sections addObject:cryptograhpySection];
+ [sections addObject:cryptographySection];
}
#ifdef CROSS_SIGNING_AND_BACKUP_DEV
diff --git a/Riot/Modules/SideMenu/SideMenuCoordinator.swift b/Riot/Modules/SideMenu/SideMenuCoordinator.swift
index 797e20818..7e3857d5f 100644
--- a/Riot/Modules/SideMenu/SideMenuCoordinator.swift
+++ b/Riot/Modules/SideMenu/SideMenuCoordinator.swift
@@ -64,6 +64,9 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
private var exploreRoomCoordinator: ExploreRoomCoordinator?
private var membersCoordinator: SpaceMembersCoordinator?
+ private var createSpaceCoordinator: SpaceCreationCoordinator?
+ private var createRoomCoordinator: CreateRoomCoordinator?
+ private var spaceSettingsCoordinator: Coordinator?
// MARK: Public
@@ -257,6 +260,72 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
self.spaceDetailPresenter.present(forSpaceWithId: spaceId, from: self.sideMenuViewController, sourceView: sourceView, session: session, animated: true)
}
+ @available(iOS 14.0, *)
+ private func showCreateSpace() {
+ guard let session = self.parameters.userSessionsService.mainUserSession?.matrixSession else {
+ return
+ }
+
+ let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session))
+ let presentable = coordinator.toPresentable()
+ presentable.presentationController?.delegate = self
+ self.sideMenuViewController.present(presentable, animated: true, completion: nil)
+ coordinator.callback = { [weak self] result in
+ guard let self = self else {
+ return
+ }
+
+ self.createSpaceCoordinator?.toPresentable().dismiss(animated: true) {
+ self.createSpaceCoordinator = nil
+ switch result {
+ case .cancel:
+ break
+ case .done(let spaceId):
+ self.select(spaceWithId: spaceId)
+ }
+ }
+ }
+ coordinator.start()
+
+ self.createSpaceCoordinator = coordinator
+ }
+
+ private func showAddRoom(spaceId: String, session: MXSession) {
+ let space = session.spaceService.getSpace(withId: spaceId)
+ let createRoomCoordinator = CreateRoomCoordinator(parameters: CreateRoomCoordinatorParameter(session: session, parentSpace: space))
+ createRoomCoordinator.delegate = self
+ let presentable = createRoomCoordinator.toPresentable()
+ presentable.presentationController?.delegate = self
+ toPresentable().present(presentable, animated: true, completion: nil)
+ createRoomCoordinator.start()
+ self.createRoomCoordinator = createRoomCoordinator
+ }
+
+ @available(iOS 14.0, *)
+ private func showSpaceSettings(spaceId: String, session: MXSession) {
+ let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: spaceId))
+ coordinator.callback = { [weak self] result in
+ guard let self = self else { return }
+
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.spaceSettingsCoordinator = nil
+ self.resetExploringSpaceIfNeeded()
+ }
+ }
+
+ let presentable = coordinator.toPresentable()
+ presentable.presentationController?.delegate = self
+ toPresentable().present(presentable, animated: true, completion: nil)
+ coordinator.start()
+ self.spaceSettingsCoordinator = coordinator
+ }
+
+ private func resetExploringSpaceIfNeeded() {
+ if sideMenuNavigationViewController.presentedViewController == nil {
+ Analytics.shared.exploringSpace = nil
+ }
+ }
+
// MARK: UserSessions management
private func registerUserSessionsServiceNotifications() {
@@ -310,7 +379,7 @@ extension SideMenuCoordinator: SideMenuNavigationControllerDelegate {
// MARK: - SideMenuNavigationControllerDelegate
extension SideMenuCoordinator: SpaceListCoordinatorDelegate {
- func spaceListCoordinatorDidSelectHomeSpace(_ coordinator: SpaceListCoordinatorType) {
+ func spaceListCoordinatorDidSelectHomeSpace(_ coordinator: SpaceListCoordinatorType) {
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
}
@@ -331,6 +400,12 @@ extension SideMenuCoordinator: SpaceListCoordinatorDelegate {
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView) {
self.showMenu(forSpaceWithId: spaceId, from: sourceView)
}
+
+ func spaceListCoordinatorDidSelectCreateSpace(_ coordinator: SpaceListCoordinatorType) {
+ if #available(iOS 14.0, *) {
+ self.showCreateSpace()
+ }
+ }
}
// MARK: - SpaceMenuPresenterDelegate
@@ -339,9 +414,29 @@ extension SideMenuCoordinator: SpaceMenuPresenterDelegate {
presenter.dismiss(animated: false) {
switch action {
case .exploreRooms:
+ Analytics.shared.viewRoomTrigger = .spaceMenu
self.showExploreRooms(spaceId: spaceId, session: session)
case .exploreMembers:
+ Analytics.shared.viewRoomTrigger = .spaceMenu
self.showMembers(spaceId: spaceId, session: session)
+ case .addRoom:
+ session.spaceService.getSpace(withId: spaceId)?.canAddRoom { canAddRoom in
+ if canAddRoom {
+ self.showAddRoom(spaceId: spaceId, session: session)
+ } else {
+ let alert = UIAlertController(title: VectorL10n.spacesAddRoom, message: VectorL10n.spacesAddRoomMissingPermissionMessage, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil))
+ self.toPresentable().present(alert, animated: true, completion: nil)
+ }
+ }
+ case .addSpace:
+ AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddSpace, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
+ case .settings:
+ if #available(iOS 14.0, *) {
+ self.showSpaceSettings(spaceId: spaceId, session: session)
+ } else {
+ AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.settingsTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
+ }
}
}
}
@@ -367,6 +462,7 @@ extension SideMenuCoordinator: ExploreRoomCoordinatorDelegate {
func exploreRoomCoordinatorDidComplete(_ coordinator: ExploreRoomCoordinatorType) {
self.exploreRoomCoordinator?.toPresentable().dismiss(animated: true) {
self.exploreRoomCoordinator = nil
+ self.resetExploringSpaceIfNeeded()
}
}
}
@@ -376,6 +472,41 @@ extension SideMenuCoordinator: SpaceMembersCoordinatorDelegate {
func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) {
self.membersCoordinator?.toPresentable().dismiss(animated: true) {
self.membersCoordinator = nil
+ self.resetExploringSpaceIfNeeded()
+ }
+ }
+}
+
+// MARK: - CreateRoomCoordinatorDelegate
+extension SideMenuCoordinator: CreateRoomCoordinatorDelegate {
+ func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom) {
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.createRoomCoordinator = nil
+ self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
+ self.resetExploringSpaceIfNeeded()
+ }
+ if let spaceId = coordinator.parentSpace?.spaceId {
+ self.parameters.appNavigator.navigate(to: .space(spaceId))
+ }
+ }
+ }
+
+ func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) {
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.createRoomCoordinator = nil
+ self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
+ self.resetExploringSpaceIfNeeded()
+ }
+ if let spaceId = coordinator.parentSpace?.spaceId {
+ self.parameters.appNavigator.navigate(to: .space(spaceId))
+ }
+ }
+ }
+
+ func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) {
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.createRoomCoordinator = nil
+ self.resetExploringSpaceIfNeeded()
}
}
}
@@ -386,5 +517,9 @@ extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.exploreRoomCoordinator = nil
self.membersCoordinator = nil
+ self.createSpaceCoordinator = nil
+ self.createRoomCoordinator = nil
+ self.spaceSettingsCoordinator = nil
+ self.resetExploringSpaceIfNeeded()
}
}
diff --git a/Riot/Modules/SideMenu/SideMenuViewAction.swift b/Riot/Modules/SideMenu/SideMenuViewAction.swift
index b494e07dc..45adc68fa 100644
--- a/Riot/Modules/SideMenu/SideMenuViewAction.swift
+++ b/Riot/Modules/SideMenu/SideMenuViewAction.swift
@@ -22,4 +22,5 @@ import Foundation
enum SideMenuViewAction {
case loadData
case tap(menuItem: SideMenuItem, sourceView: UIView)
+ case tapHeader(sourceView: UIView)
}
diff --git a/Riot/Modules/SideMenu/SideMenuViewController.storyboard b/Riot/Modules/SideMenu/SideMenuViewController.storyboard
index 998474b8d..579a3332f 100644
--- a/Riot/Modules/SideMenu/SideMenuViewController.storyboard
+++ b/Riot/Modules/SideMenu/SideMenuViewController.storyboard
@@ -1,9 +1,9 @@
-
+
-
+
@@ -28,29 +28,42 @@
-
+
+
+
+
+
+
@@ -114,7 +127,7 @@
-
+
diff --git a/Riot/Modules/SideMenu/SideMenuViewController.swift b/Riot/Modules/SideMenu/SideMenuViewController.swift
index f92be3bbd..344fb3764 100644
--- a/Riot/Modules/SideMenu/SideMenuViewController.swift
+++ b/Riot/Modules/SideMenu/SideMenuViewController.swift
@@ -198,6 +198,10 @@ final class SideMenuViewController: UIViewController {
// MARK: - Actions
+
+ @IBAction func headerTapAction(sender: UIView) {
+ self.viewModel.process(viewAction: .tapHeader(sourceView: sender))
+ }
}
// MARK: - SideMenuViewModelViewDelegate
diff --git a/Riot/Modules/SideMenu/SideMenuViewModel.swift b/Riot/Modules/SideMenu/SideMenuViewModel.swift
index 7c9d128ee..0b0b84862 100644
--- a/Riot/Modules/SideMenu/SideMenuViewModel.swift
+++ b/Riot/Modules/SideMenu/SideMenuViewModel.swift
@@ -53,6 +53,8 @@ final class SideMenuViewModel: SideMenuViewModelType {
self.loadData()
case .tap(menuItem: let menuItem, sourceView: let sourceView):
self.coordinatorDelegate?.sideMenuViewModel(self, didTapMenuItem: menuItem, fromSourceView: sourceView)
+ case .tapHeader(sourceView: let sourceView):
+ self.coordinatorDelegate?.sideMenuViewModel(self, didTapMenuItem: .settings, fromSourceView: sourceView)
}
}
@@ -105,7 +107,6 @@ final class SideMenuViewModel: SideMenuViewModelType {
sideMenuItems += [
.settings,
- .help,
.feedback
]
diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListCoordinator.swift b/Riot/Modules/Spaces/SpaceList/SpaceListCoordinator.swift
index 5d6924347..e8cfc5db5 100644
--- a/Riot/Modules/Spaces/SpaceList/SpaceListCoordinator.swift
+++ b/Riot/Modules/Spaces/SpaceList/SpaceListCoordinator.swift
@@ -85,4 +85,9 @@ extension SpaceListCoordinator: SpaceListViewModelCoordinatorDelegate {
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView) {
self.delegate?.spaceListCoordinator(self, didPressMoreForSpaceWithId: spaceId, from: sourceView)
}
+
+ func spaceListViewModelDidSelectCreateSpace(_ viewModel: SpaceListViewModelType) {
+ self.delegate?.spaceListCoordinatorDidSelectCreateSpace(self)
+ }
+
}
diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListCoordinatorType.swift b/Riot/Modules/Spaces/SpaceList/SpaceListCoordinatorType.swift
index 8ab4815bc..03383216e 100644
--- a/Riot/Modules/Spaces/SpaceList/SpaceListCoordinatorType.swift
+++ b/Riot/Modules/Spaces/SpaceList/SpaceListCoordinatorType.swift
@@ -23,6 +23,7 @@ protocol SpaceListCoordinatorDelegate: AnyObject {
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectSpaceWithId spaceId: String)
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectInviteWithId spaceId: String, from sourceView: UIView?)
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView)
+ func spaceListCoordinatorDidSelectCreateSpace(_ coordinator: SpaceListCoordinatorType)
}
/// `SpaceListCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListSection.swift b/Riot/Modules/Spaces/SpaceList/SpaceListSection.swift
index f4cc0c399..8877dbac2 100644
--- a/Riot/Modules/Spaces/SpaceList/SpaceListSection.swift
+++ b/Riot/Modules/Spaces/SpaceList/SpaceListSection.swift
@@ -20,4 +20,5 @@ import Foundation
enum SpaceListSection {
case home(_ viewData: SpaceListItemViewData)
case spaces(_ viewDataList: [SpaceListItemViewData])
+ case addSpace(_ viewData: SpaceListItemViewData)
}
diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift
index eaf3a5e09..b952b336c 100644
--- a/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift
+++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift
@@ -58,7 +58,7 @@ final class SpaceListViewCell: UITableViewCell, Themable, NibReusable {
func fill(with viewData: SpaceListItemViewData) {
self.avatarView.fill(with: viewData.avatarViewData)
self.titleLabel.text = viewData.title
- self.moreButton.isHidden = viewData.isInvite
+ self.moreButton.isHidden = viewData.spaceId == SpaceListViewModel.Constants.addSpaceId || viewData.isInvite
if viewData.isInvite {
self.isBadgeAlert = true
self.badgeLabel.isHidden = false
@@ -68,7 +68,7 @@ final class SpaceListViewCell: UITableViewCell, Themable, NibReusable {
self.badgeLabel.text = "!"
} else {
self.isBadgeAlert = viewData.highlightedNotificationCount > 0
- let notificationCount = viewData.notificationCount + viewData.highlightedNotificationCount
+ let notificationCount = viewData.notificationCount
self.badgeLabel.isHidden = notificationCount == 0
if let theme = self.theme {
self.badgeLabel.badgeColor = viewData.highlightedNotificationCount == 0 ? theme.colors.tertiaryContent : theme.colors.alert
diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift
index b89d314b2..2e3586048 100644
--- a/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift
+++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift
@@ -169,8 +169,10 @@ extension SpaceListViewController: UITableViewDataSource {
numberOfRows = 1
case .spaces(let viewDataList):
numberOfRows = viewDataList.count
+ case .addSpace:
+ numberOfRows = 1
}
-
+
return numberOfRows
}
@@ -186,6 +188,8 @@ extension SpaceListViewController: UITableViewDataSource {
viewData = spaceViewData
case .spaces(let viewDataList):
viewData = viewDataList[indexPath.row]
+ case .addSpace(let spaceViewData):
+ viewData = spaceViewData
}
cell.update(theme: self.theme)
diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift
index 4d82d8368..78594ac5f 100644
--- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift
+++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift
@@ -24,6 +24,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
enum Constants {
static let homeSpaceId: String = "home"
+ static let addSpaceId: String = "add_space"
}
// MARK: - Properties
@@ -36,7 +37,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
private var sections: [SpaceListSection] = []
private var selectedIndexPath: IndexPath = IndexPath(row: 0, section: 0) {
didSet {
- self.selectedItemId = self.itemId(with: self.selectedIndexPath)
+ self.selectedItemId = self.itemId(with: self.selectedIndexPath) ?? Constants.homeSpaceId
}
}
private var homeIndexPath: IndexPath = IndexPath(row: 0, section: 0)
@@ -55,6 +56,8 @@ final class SpaceListViewModel: SpaceListViewModelType {
NotificationCenter.default.addObserver(self, selector: #selector(self.sessionDidSync(notification:)), name: MXSpaceService.didBuildSpaceGraph, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.counterDidUpdateNotificationCount(notification:)), name: MXSpaceNotificationCounter.didUpdateNotificationCount, object: nil)
+
+ NotificationCenter.default.addObserver(self, selector: #selector(self.loadData), name: NSNotification.Name.themeServiceDidChangeTheme, object: nil)
}
@@ -70,13 +73,16 @@ final class SpaceListViewModel: SpaceListViewModelType {
self.loadData()
case .selectRow(at: let indexPath, from: let sourceView):
guard self.selectedIndexPath != indexPath else {
+ Analytics.shared.trackInteraction(.spacePanelSelectedSpace)
return
}
+
let section = self.sections[indexPath.section]
switch section {
case .home:
self.selectHome()
self.selectedIndexPath = indexPath
+ Analytics.shared.trackInteraction(.spacePanelSwitchSpace)
self.update(viewState: .selectionChanged(indexPath))
case .spaces(let viewDataList):
let spaceViewData = viewDataList[indexPath.row]
@@ -85,14 +91,19 @@ final class SpaceListViewModel: SpaceListViewModelType {
} else {
self.selectSpace(with: spaceViewData.spaceId)
self.selectedIndexPath = indexPath
+ Analytics.shared.trackInteraction(.spacePanelSwitchSpace)
self.update(viewState: .selectionChanged(indexPath))
}
+ case .addSpace:
+ self.update(viewState: .selectionChanged(self.selectedIndexPath))
+ addSpace()
}
case .moreAction(at: let indexPath, from: let sourceView):
let section = self.sections[indexPath.section]
switch section {
case .home:
self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: Constants.homeSpaceId, from: sourceView)
+ case .addSpace: break
case .spaces(let viewDataList):
let spaceViewData = viewDataList[indexPath.row]
self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: spaceViewData.spaceId, from: sourceView)
@@ -108,6 +119,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
for (sectionIndex, section) in self.sections.enumerated() {
switch section {
case .home: break
+ case .addSpace: break
case .spaces(let viewDataList):
for (row, itemViewData) in viewDataList.enumerated() where itemViewData.spaceId == spaceId {
let indexPath = IndexPath(row: row, section: sectionIndex)
@@ -129,7 +141,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
loadData()
}
- private func loadData() {
+ @objc private func loadData() {
guard let session = self.userSessionsService.mainUserSession?.matrixSession else {
// If there is no main session, reset current selection and give an empty section list
// It can happen when the user make a clear cache or logout
@@ -142,7 +154,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
let homeViewData = self.createHomeViewData(session: session)
let viewDataList = getSpacesViewData(session: session)
- let sections: [SpaceListSection] = viewDataList.invites.isEmpty ? [
+ var sections: [SpaceListSection] = viewDataList.invites.isEmpty ? [
.home(homeViewData),
.spaces(viewDataList.spaces)
]
@@ -152,6 +164,12 @@ final class SpaceListViewModel: SpaceListViewModelType {
.home(homeViewData),
.spaces(viewDataList.spaces)
]
+
+ let spacesSectionIndex = sections.count - 1
+ if #available(iOS 14.0, *) {
+ let addSpaceViewData = self.createAddSpaceViewData(session: session)
+ sections.append(.addSpace(addSpaceViewData))
+ }
self.sections = sections
let homeIndexPath = viewDataList.invites.isEmpty ? IndexPath(row: 0, section: 0) : IndexPath(row: 0, section: 1)
@@ -159,10 +177,9 @@ final class SpaceListViewModel: SpaceListViewModelType {
self.selectedIndexPath = homeIndexPath
} else if self.selectedItemId != self.itemId(with: self.selectedIndexPath) {
var newSelection: IndexPath?
- let section = sections.last
+ let section = sections[spacesSectionIndex]
switch section {
- case .home:
- break
+ case .home, .addSpace: break
case .spaces(let viewDataList):
var index = 0
for itemViewData in viewDataList {
@@ -171,8 +188,6 @@ final class SpaceListViewModel: SpaceListViewModelType {
}
index += 1
}
- case .none:
- break
}
if let selection = newSelection {
@@ -191,6 +206,10 @@ final class SpaceListViewModel: SpaceListViewModelType {
self.coordinatorDelegate?.spaceListViewModelDidSelectHomeSpace(self)
}
+ private func addSpace() {
+ self.coordinatorDelegate?.spaceListViewModelDidSelectCreateSpace(self)
+ }
+
private func selectSpace(with spaceId: String) {
self.coordinatorDelegate?.spaceListViewModel(self, didSelectSpaceWithId: spaceId)
}
@@ -200,7 +219,8 @@ final class SpaceListViewModel: SpaceListViewModelType {
}
private func createHomeViewData(session: MXSession) -> SpaceListItemViewData {
- let avatarViewData = AvatarViewData(matrixItemId: Constants.homeSpaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(Asset.Images.spaceHomeIcon.image, .center))
+ let defaultAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.spaceHomeIconDark : Asset.Images.spaceHomeIconLight
+ let avatarViewData = AvatarViewData(matrixItemId: Constants.homeSpaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(defaultAsset.image, .center))
let homeNotificationState = session.spaceService.notificationCounter.homeNotificationState
let homeViewData = SpaceListItemViewData(spaceId: Constants.homeSpaceId,
@@ -212,6 +232,19 @@ final class SpaceListViewModel: SpaceListViewModelType {
return homeViewData
}
+ private func createAddSpaceViewData(session: MXSession) -> SpaceListItemViewData {
+ let defaultAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.spacesAddSpaceDark : Asset.Images.spacesAddSpaceLight
+ let avatarViewData = AvatarViewData(matrixItemId: Constants.addSpaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(defaultAsset.image, .center))
+
+ let homeViewData = SpaceListItemViewData(spaceId: Constants.addSpaceId,
+ title: VectorL10n.spacesAddSpaceTitle,
+ avatarViewData: avatarViewData,
+ isInvite: false,
+ notificationCount: 0,
+ highlightedNotificationCount: 0)
+ return homeViewData
+ }
+
private func getSpacesViewData(session: MXSession) -> (invites: [SpaceListItemViewData], spaces: [SpaceListItemViewData]) {
var invites: [SpaceListItemViewData] = []
var spaces: [SpaceListItemViewData] = []
@@ -241,7 +274,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
self.currentOperation?.cancel()
}
- private func itemId(with indexPath: IndexPath) -> String {
+ private func itemId(with indexPath: IndexPath) -> String? {
guard self.selectedIndexPath.section < self.sections.count else {
return Constants.homeSpaceId
}
@@ -250,8 +283,13 @@ final class SpaceListViewModel: SpaceListViewModelType {
case .home:
return Constants.homeSpaceId
case .spaces(let viewDataList):
+ guard self.selectedIndexPath.row < viewDataList.count else {
+ return nil
+ }
let spaceViewData = viewDataList[self.selectedIndexPath.row]
return spaceViewData.spaceId
+ case .addSpace:
+ return Constants.addSpaceId
}
}
diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModelType.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModelType.swift
index 00066a70d..cb706a8c2 100644
--- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModelType.swift
+++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModelType.swift
@@ -27,6 +27,7 @@ protocol SpaceListViewModelCoordinatorDelegate: AnyObject {
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didSelectSpaceWithId spaceId: String)
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didSelectInviteWithId spaceId: String, from sourceView: UIView?)
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView)
+ func spaceListViewModelDidSelectCreateSpace(_ viewModel: SpaceListViewModelType)
}
/// Protocol describing the view model used by `SpaceListViewController`
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailCoordinator.swift b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailCoordinator.swift
index fa62c75b5..a0b466276 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailCoordinator.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailCoordinator.swift
@@ -24,6 +24,7 @@ struct SpaceMemberDetailCoordinatorParameters {
let member: MXRoomMember
let session: MXSession
let spaceId: String
+ let showCancelMenuItem: Bool
}
final class SpaceMemberDetailCoordinator: NSObject, SpaceMemberDetailCoordinatorType {
@@ -49,7 +50,7 @@ final class SpaceMemberDetailCoordinator: NSObject, SpaceMemberDetailCoordinator
init(parameters: SpaceMemberDetailCoordinatorParameters) {
self.parameters = parameters
- let spaceMemberDetailViewModel = SpaceMemberDetailViewModel(userSessionsService: parameters.userSessionsService, session: parameters.session, member: parameters.member, spaceId: parameters.spaceId)
+ let spaceMemberDetailViewModel = SpaceMemberDetailViewModel(userSessionsService: parameters.userSessionsService, session: parameters.session, member: parameters.member, spaceId: parameters.spaceId, showCancelMenuItem: parameters.showCancelMenuItem)
let spaceMemberDetailViewController = SpaceMemberDetailViewController.instantiate(with: spaceMemberDetailViewModel)
spaceMemberDetailViewController.enableMention = true
spaceMemberDetailViewController.enableVoipCall = false
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewController.swift
index 136419f8d..999e9fb85 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewController.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewController.swift
@@ -106,11 +106,13 @@ final class SpaceMemberDetailViewController: RoomMemberDetailsViewController {
}
private func setupViews() {
- let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
- self?.cancelButtonAction()
+ if viewModel.showCancelMenuItem {
+ let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
+ self?.cancelButtonAction()
+ }
+
+ self.navigationItem.rightBarButtonItem = cancelBarButtonItem
}
-
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
}
private func render(viewState: SpaceMemberDetailViewState) {
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift
index f462dda89..cee72964d 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift
@@ -29,6 +29,7 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
private let member: MXRoomMember
private let spaceId: String
private var space: MXSpace?
+ private(set) var showCancelMenuItem: Bool
private var currentOperation: MXHTTPOperation?
@@ -39,11 +40,12 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
// MARK: - Setup
- init(userSessionsService: UserSessionsService, session: MXSession, member: MXRoomMember, spaceId: String) {
+ init(userSessionsService: UserSessionsService, session: MXSession, member: MXRoomMember, spaceId: String, showCancelMenuItem: Bool) {
self.userSessionsService = userSessionsService
self.session = session
self.member = member
self.spaceId = spaceId
+ self.showCancelMenuItem = showCancelMenuItem
}
deinit {
@@ -57,6 +59,7 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
case .loadData:
self.loadData()
case .openRoom(let roomId):
+ Analytics.shared.viewRoomTrigger = .spaceMemberDetail
self.coordinatorDelegate?.spaceMemberDetailViewModel(self, showRoomWithId: roomId)
case .createRoom(let memberId):
self.createDirectRoom(forMemberWithId: memberId)
@@ -106,6 +109,7 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
}
return
}
+ Analytics.shared.viewRoomTrigger = .created
self.coordinatorDelegate?.spaceMemberDetailViewModel(self, showRoomWithId: room.roomId)
}
} failure: { error in
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModelType.swift b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModelType.swift
index 86d7e8b7d..009f47b33 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModelType.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModelType.swift
@@ -32,6 +32,7 @@ protocol SpaceMemberDetailViewModelType {
var viewDelegate: SpaceMemberDetailViewModelViewDelegate? { get set }
var coordinatorDelegate: SpaceMemberDetailViewModelCoordinatorDelegate? { get set }
+ var showCancelMenuItem: Bool { get }
func process(viewAction: SpaceMemberDetailViewAction)
}
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/AddItemHeaderView.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/AddItemHeaderView.swift
new file mode 100644
index 000000000..41b207a17
--- /dev/null
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/AddItemHeaderView.swift
@@ -0,0 +1,90 @@
+//
+// Copyright 2020 New Vector Ltd
+//
+// 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 UIKit
+import Reusable
+
+@objc
+protocol AddItemHeaderViewDelegate: AnyObject {
+ func addItemHeaderView(_ headerView: AddItemHeaderView, didTapButton button: UIButton)
+}
+
+/// `AddItemHeaderView` is a generic view used as a header view for UITableView.
+/// With this view we can add an extra action cell with icon and text as for SpaceMemberList and SpaceExploreRooms
+@objcMembers
+final class AddItemHeaderView: UIView, NibLoadable, Themable {
+
+ // MARK: - Constants
+
+ private enum Constants {
+ static let buttonHighlightedAlpha: CGFloat = 0.2
+ }
+
+ // MARK: - Properties
+
+ @IBOutlet private weak var button: UIButton!
+ @IBOutlet private weak var iconBackgroundView: UIView!
+ @IBOutlet private weak var iconView: UIImageView!
+ @IBOutlet private weak var titleLabel: UILabel!
+
+ weak var delegate: AddItemHeaderViewDelegate?
+
+ private var title: String? {
+ didSet {
+ titleLabel.text = title
+ }
+ }
+ private var icon: UIImage? {
+ didSet {
+ iconView.image = icon
+ }
+ }
+
+ // MARK: - Setup
+
+ static func instantiate(title: String?, icon: UIImage?) -> AddItemHeaderView {
+ let view = AddItemHeaderView.loadFromNib()
+ view.icon = icon
+ view.title = title
+ view.update(theme: ThemeService.shared().theme)
+ return view
+ }
+
+ // MARK: - Life cycle
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
+ iconBackgroundView.layer.masksToBounds = true
+ iconBackgroundView.layer.cornerRadius = iconBackgroundView.bounds.width / 2
+ }
+
+ // MARK: - Public
+
+ func update(theme: Theme) {
+ iconBackgroundView.layer.backgroundColor = theme.colors.quinaryContent.cgColor
+ iconView.tintColor = theme.colors.secondaryContent
+ titleLabel.textColor = theme.colors.primaryContent
+ titleLabel.font = theme.fonts.headline
+ }
+
+ // MARK: - Action
+
+ @objc private func buttonAction(_ sender: UIButton) {
+ delegate?.addItemHeaderView(self, didTapButton: button)
+ }
+}
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/AddItemHeaderView.xib b/Riot/Modules/Spaces/SpaceMembers/MemberList/AddItemHeaderView.xib
new file mode 100644
index 000000000..0fb31911b
--- /dev/null
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/AddItemHeaderView.xib
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinator.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinator.swift
index 77ec12f62..76ae70b78 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinator.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinator.swift
@@ -70,4 +70,8 @@ extension SpaceMemberListCoordinator: SpaceMemberListViewModelCoordinatorDelegat
func spaceMemberListViewModelDidCancel(_ viewModel: SpaceMemberListViewModelType) {
self.delegate?.spaceMemberListCoordinatorDidCancel(self)
}
+
+ func spaceMemberListViewModelShowInvite(_ viewModel: SpaceMemberListViewModelType) {
+ self.delegate?.spaceMemberListCoordinatorShowInvite(self)
+ }
}
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinatorType.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinatorType.swift
index 43225f787..d933457bd 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinatorType.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListCoordinatorType.swift
@@ -21,6 +21,7 @@ import Foundation
protocol SpaceMemberListCoordinatorDelegate: AnyObject {
func spaceMemberListCoordinator(_ coordinator: SpaceMemberListCoordinatorType, didSelect member: MXRoomMember, from sourceView: UIView?)
func spaceMemberListCoordinatorDidCancel(_ coordinator: SpaceMemberListCoordinatorType)
+ func spaceMemberListCoordinatorShowInvite(_ coordinator: SpaceMemberListCoordinatorType)
}
/// `SpaceMemberListCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewAction.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewAction.swift
index 83118ce29..71b899a25 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewAction.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewAction.swift
@@ -23,4 +23,5 @@ enum SpaceMemberListViewAction {
case loadData
case complete(_ selectedMember: MXRoomMember, _ sourceView: UIView?)
case cancel
+ case invite
}
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift
index c46b572e6..dce516a80 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift
@@ -35,20 +35,16 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var titleView: MainTitleView!
- private var emptyView: SearchEmptyView!
+ private let inviteHeaderView = AddItemHeaderView.instantiate(title: VectorL10n.spacesInvitePeople, icon: Asset.Images.spaceInviteUser.image)
- private var emptyViewArtwork: UIImage {
- return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.peopleEmptyScreenArtworkDark.image : Asset.Images.peopleEmptyScreenArtwork.image
- }
-
// MARK: - Setup
class func instantiate(with viewModel: SpaceMemberListViewModelType) -> SpaceMemberListViewController {
let viewController = SpaceMemberListViewController()
viewController.viewModel = viewModel
viewController.showParticipantCustomAccessoryView = false
+ viewController.showInviteUserFab = false
viewController.theme = ThemeService.shared().theme
- viewController.emptyView = SearchEmptyView()
return viewController
}
@@ -71,12 +67,15 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
self.viewModel.process(viewAction: .loadData)
self.title = ""
+
+ self.setupTableViewHeader()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
AnalyticsScreenTracker.trackScreen(.spaceMembers)
+ Analytics.shared.exploringSpace = viewModel.space
}
override var preferredStatusBarStyle: UIStatusBarStyle {
@@ -84,7 +83,12 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
}
// MARK: - Private
-
+
+ private func setupTableViewHeader() {
+ inviteHeaderView.delegate = self
+ tableView.tableHeaderView = inviteHeaderView
+ }
+
private func update(theme: Theme) {
self.theme = theme
@@ -96,7 +100,8 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
theme.applyStyle(onSearchBar: self.searchBarView)
self.titleView.update(theme: theme)
- self.emptyView.update(theme: theme)
+
+ self.inviteHeaderView.update(theme: theme)
}
private func registerThemeServiceDidChangeThemeNotification() {
@@ -112,16 +117,11 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
self?.cancelButtonAction()
}
- self.navigationItem.rightBarButtonItem = cancelBarButtonItem
+ self.navigationItem.leftBarButtonItem = cancelBarButtonItem
self.titleView = MainTitleView()
self.titleView.titleLabel.text = VectorL10n.roomDetailsPeople
self.navigationItem.titleView = self.titleView
-
- self.emptyView.frame = CGRect(x: Constants.emptySearchViewMargin, y: self.searchBarView.frame.maxY + 2 * Constants.emptySearchViewMargin, width: self.view.bounds.width - 2 * Constants.emptySearchViewMargin, height: 0)
- self.emptyView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- self.emptyView.alpha = 0
- self.view.insertSubview(self.emptyView, at: 0)
}
private func render(viewState: SpaceMemberListViewState) {
@@ -143,9 +143,6 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.mxRoom = space.room
self.titleView.subtitleLabel.text = space.summary?.displayname
- self.emptyView.titleLabel.text = VectorL10n.spacesNoResultFoundTitle
- self.emptyView.detailLabel.text = VectorL10n.spacesNoMemberFoundDetail(space.summary?.displayname ?? "")
- self.emptyView.layoutIfNeeded()
}
private func render(error: Error) {
@@ -160,7 +157,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
// MARK: - Actions
@objc private func onAddParticipantButtonPressed() {
- self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil)
+ self.viewModel.process(viewAction: .invite)
}
private func cancelButtonAction() {
@@ -179,11 +176,6 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
override func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
super.searchBar(searchBar, textDidChange: searchText)
-
- UIView.animate(withDuration: 0.2) {
- self.emptyView.alpha = self.tableView.numberOfSections == 0 ? 1 : 0
- self.tableView.alpha = self.tableView.numberOfSections == 0 ? 0 : 1
- }
}
// MARK: - MXKRoomMemberDetailsViewControllerDelegate
@@ -206,3 +198,10 @@ extension SpaceMemberListViewController: SpaceMemberListViewModelViewDelegate {
self.render(viewState: viewSate)
}
}
+
+// MARK: - SpaceMemberListViewModelViewDelegate
+extension SpaceMemberListViewController: AddItemHeaderViewDelegate {
+ func addItemHeaderView(_ headerView: AddItemHeaderView, didTapButton button: UIButton) {
+ self.viewModel.process(viewAction: .invite)
+ }
+}
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift
index c8d7ec1fd..362bf56df 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift
@@ -32,6 +32,9 @@ final class SpaceMemberListViewModel: SpaceMemberListViewModelType {
// MARK: Public
+ var space: MXSpace? {
+ return session.spaceService.getSpace(withId: spaceId)
+ }
weak var viewDelegate: SpaceMemberListViewModelViewDelegate?
weak var coordinatorDelegate: SpaceMemberListViewModelCoordinatorDelegate?
@@ -57,6 +60,8 @@ final class SpaceMemberListViewModel: SpaceMemberListViewModelType {
case .cancel:
self.cancelOperations()
self.coordinatorDelegate?.spaceMemberListViewModelDidCancel(self)
+ case .invite:
+ self.coordinatorDelegate?.spaceMemberListViewModelShowInvite(self)
}
}
diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift
index a07ceb55e..3cbecf47f 100644
--- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift
@@ -25,6 +25,7 @@ protocol SpaceMemberListViewModelViewDelegate: AnyObject {
protocol SpaceMemberListViewModelCoordinatorDelegate: AnyObject {
func spaceMemberListViewModel(_ viewModel: SpaceMemberListViewModelType, didSelect member: MXRoomMember, from sourceView: UIView?)
func spaceMemberListViewModelDidCancel(_ viewModel: SpaceMemberListViewModelType)
+ func spaceMemberListViewModelShowInvite(_ viewModel: SpaceMemberListViewModelType)
}
/// Protocol describing the view model used by `SpaceMemberListViewController`
@@ -32,6 +33,7 @@ protocol SpaceMemberListViewModelType {
var viewDelegate: SpaceMemberListViewModelViewDelegate? { get set }
var coordinatorDelegate: SpaceMemberListViewModelCoordinatorDelegate? { get set }
+ var space: MXSpace? { get }
func process(viewAction: SpaceMemberListViewAction)
}
diff --git a/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift b/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift
index 9a0c44ac9..f9dde8858 100644
--- a/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift
+++ b/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift
@@ -22,6 +22,17 @@ struct SpaceMembersCoordinatorParameters {
let userSessionsService: UserSessionsService
let session: MXSession
let spaceId: String
+ let navigationRouter: NavigationRouterType
+
+ init(userSessionsService: UserSessionsService,
+ session: MXSession,
+ spaceId: String,
+ navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) {
+ self.userSessionsService = userSessionsService
+ self.session = session
+ self.spaceId = spaceId
+ self.navigationRouter = navigationRouter
+ }
}
@objcMembers
@@ -46,7 +57,7 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
init(parameters: SpaceMembersCoordinatorParameters) {
self.parameters = parameters
- self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
+ self.navigationRouter = parameters.navigationRouter
}
// MARK: - Public methods
@@ -59,8 +70,15 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
self.add(childCoordinator: rootCoordinator)
- self.navigationRouter.setRootModule(rootCoordinator)
- }
+ if self.navigationRouter.modules.isEmpty {
+ self.navigationRouter.setRootModule(rootCoordinator)
+ } else {
+ self.navigationRouter.push(rootCoordinator, animated: true) {
+ self.remove(childCoordinator: rootCoordinator)
+ }
+ }
+
+ }
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
@@ -99,7 +117,7 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
}
private func createSpaceMemberDetailCoordinator(with member: MXRoomMember) -> SpaceMemberDetailCoordinator {
- let parameters = SpaceMemberDetailCoordinatorParameters(userSessionsService: self.parameters.userSessionsService, member: member, session: self.parameters.session, spaceId: self.parameters.spaceId)
+ let parameters = SpaceMemberDetailCoordinatorParameters(userSessionsService: self.parameters.userSessionsService, member: member, session: self.parameters.session, spaceId: self.parameters.spaceId, showCancelMenuItem: false)
let coordinator = SpaceMemberDetailCoordinator(parameters: parameters)
coordinator.delegate = self
return coordinator
@@ -109,6 +127,10 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
let roomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session)
roomDataSourceManager?.roomDataSource(forRoom: roomId, create: true, onComplete: { [weak self] roomDataSource in
+ if let room = self?.parameters.session.room(withRoomId: roomId) {
+ Analytics.shared.trackViewRoom(room)
+ }
+
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
guard let roomViewController = storyboard.instantiateViewController(withIdentifier: "RoomViewControllerStoryboardId") as? RoomViewController else {
return
@@ -131,8 +153,52 @@ extension SpaceMembersCoordinator: SpaceMemberListCoordinatorDelegate {
func spaceMemberListCoordinatorDidCancel(_ coordinator: SpaceMemberListCoordinatorType) {
self.delegate?.spaceMembersCoordinatorDidCancel(self)
}
+
+ func spaceMemberListCoordinatorShowInvite(_ coordinator: SpaceMemberListCoordinatorType) {
+ guard let space = parameters.session.spaceService.getSpace(withId: parameters.spaceId), let spaceRoom = space.room else {
+ MXLog.error("[SpaceMembersCoordinator] spaceMemberListCoordinatorShowInvite: failed to find space with id \(parameters.spaceId)")
+ return
+ }
+
+ spaceRoom.state { [weak self] roomState in
+ guard let self = self else { return }
+
+ guard let powerLevels = roomState?.powerLevels, let userId = self.parameters.session.myUserId else {
+ MXLog.error("[SpaceMembersCoordinator] spaceMemberListCoordinatorShowInvite: failed to find powerLevels for room")
+ return
+ }
+ let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: userId)
+
+ guard userPowerLevel >= powerLevels.invite else {
+ let alert = UIAlertController(title: VectorL10n.spacesInvitePeople, message: VectorL10n.spaceInviteNotEnoughPermission, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil))
+ self.navigationRouter.present(alert, animated: true)
+ return
+ }
+
+ let coordinator = ContactsPickerCoordinator(session: self.parameters.session, room: spaceRoom, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil)
+ coordinator.delegate = self
+ coordinator.start()
+ self.childCoordinators.append(coordinator)
+ self.navigationRouter.present(coordinator.toPresentable(), animated: true)
+ }
+ }
}
+// MARK: - ContactsPickerCoordinatorDelegate
+extension SpaceMembersCoordinator: ContactsPickerCoordinatorDelegate {
+ func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ }
+
+ func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ }
+
+ func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) {
+ remove(childCoordinator: coordinator)
+ }
+}
+
+// MARK: - SpaceMemberDetailCoordinatorDelegate
extension SpaceMembersCoordinator: SpaceMemberDetailCoordinatorDelegate {
func spaceMemberDetailCoordinator(_ coordinator: SpaceMemberDetailCoordinatorType, showRoomWithId roomId: String) {
if !UIDevice.current.isPhone, let memberDetailCoordinator = self.memberDetailCoordinator {
diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift
index 6f8b7378f..f9964201d 100644
--- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift
+++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift
@@ -21,6 +21,9 @@ enum SpaceMenuListItemAction {
case showAllRoomsInHomeSpace
case exploreSpaceMembers
case exploreSpaceRooms
+ case addRoom
+ case addSpace
+ case settings
case leaveSpace
}
@@ -42,6 +45,8 @@ class SpaceMenuListItemViewData {
let style: SpaceMenuListItemStyle
let title: String?
let icon: UIImage?
+ let isBeta: Bool
+
/// Any value related to the type of data (e.g. `Bool` for `boolean` style, `nil` for `normal` and `destructive` style)
var value: Any? {
didSet {
@@ -50,11 +55,12 @@ class SpaceMenuListItemViewData {
}
weak var delegate: SpaceMenuListItemViewDataDelegate?
- init(action: SpaceMenuListItemAction, style: SpaceMenuListItemStyle, title: String?, icon: UIImage?, value: Any?) {
+ init(action: SpaceMenuListItemAction, style: SpaceMenuListItemStyle, title: String?, icon: UIImage?, value: Any?, isBeta: Bool = false) {
self.action = action
self.style = style
self.title = title
self.icon = icon
self.value = value
+ self.isBeta = isBeta
}
}
diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift
index bc8243a9a..1ce0d2427 100644
--- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift
+++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift
@@ -23,6 +23,8 @@ class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable {
@IBOutlet private weak var iconView: UIImageView!
@IBOutlet private weak var titleLabel: UILabel!
+ @IBOutlet private weak var betaView: UIView!
+ @IBOutlet private weak var betaLabel: UILabel!
@IBOutlet private weak var selectionView: UIView!
// MARK: - Private
@@ -64,6 +66,10 @@ class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable {
self.titleLabel.textColor = theme.colors.primaryContent
self.iconView.tintColor = theme.colors.secondaryContent
}
+
+ self.betaView.layer.masksToBounds = true
+ self.betaView.layer.cornerRadius = 4
+ self.betaView.isHidden = !viewData.isBeta
}
func update(theme: Theme) {
@@ -73,5 +79,8 @@ class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable {
self.titleLabel.textColor = theme.colors.primaryContent
self.titleLabel.font = theme.fonts.body
self.selectionView.backgroundColor = theme.colors.separator
+ self.betaLabel.font = theme.fonts.caption2SB
+ self.betaLabel.textColor = theme.colors.secondaryContent
+ self.betaView.backgroundColor = theme.colors.quinaryContent
}
}
diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.xib b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.xib
index 0d71ef2c6..b6825d18e 100644
--- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.xib
+++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.xib
@@ -25,12 +25,30 @@
-