diff --git a/CHANGES.md b/CHANGES.md
index 53b527969..040d7e5b4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,10 @@
+## Changes in 1.8.7 (2022-03-18)
+
+🙌 Improvements
+
+- Room: Allow ignoring invited users that have not joined a room yet ([#5866](https://github.com/vector-im/element-ios/issues/5866))
+
+
## Changes in 1.8.6 (2022-03-14)
🙌 Improvements
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/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/location_live_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json
similarity index 56%
rename from Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json
rename to Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json
index 0a20c899a..e312c8132 100644
--- a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json
+++ b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json
@@ -1,17 +1,17 @@
{
"images" : [
{
- "filename" : "location_live_icon.png",
+ "filename" : "live_location_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "location_live_icon@2x.png",
+ "filename" : "live_location_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
- "filename" : "location_live_icon@3x.png",
+ "filename" : "live_location_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
@@ -19,5 +19,8 @@
"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/Location/location_live_icon.imageset/location_live_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon.png
deleted file mode 100644
index 6aa2e2639..000000000
Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@2x.png
deleted file mode 100644
index 4ed19fb09..000000000
Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@2x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@3x.png
deleted file mode 100644
index cabf83a92..000000000
Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/location_live_icon@3x.png and /dev/null 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/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 931f07e75..cc748d6d3 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";
@@ -75,6 +74,12 @@
"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 (%@)";
@@ -2086,13 +2091,14 @@ Tap the + to start adding people.";
"location_sharing_settings_toggle_title" = "Enable location sharing";
+// MARK: Live location sharing
+
"location_sharing_live_share_title" = "Share live location";
-
+"live_location_sharing_banner_title" = "Live location enabled";
+"live_location_sharing_banner_stop" = "Stop";
"location_sharing_static_share_title" = "Send my current location";
-
"location_sharing_pin_drop_share_title" = "Send this location";
-
// 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/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/Generated/Images.swift b/Riot/Generated/Images.swift
index d64969d94..469b4b1e7 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -124,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")
@@ -169,7 +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 locationLiveIcon = ImageAsset(name: "location_live_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")
@@ -185,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")
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 193cc718d..cb9373175 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -1651,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")
@@ -2703,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)
@@ -5731,6 +5747,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")
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/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift
index 1333864ab..74880e69f 100644
--- a/Riot/Modules/Analytics/Analytics.swift
+++ b/Riot/Modules/Analytics/Analytics.swift
@@ -121,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)
}
diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift
index fef10f5b8..e218a1a11 100755
--- a/Riot/Modules/Application/AppCoordinator.swift
+++ b/Riot/Modules/Application/AppCoordinator.swift
@@ -251,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 f7c2dff3b..85180036a 100644
--- a/Riot/Modules/Application/LegacyAppDelegate.m
+++ b/Riot/Modules/Application/LegacyAppDelegate.m
@@ -2048,7 +2048,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];
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/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/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m
index 0f51fd248..22db94190 100644
--- a/Riot/Modules/Home/HomeViewController.m
+++ b/Riot/Modules/Home/HomeViewController.m
@@ -600,9 +600,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/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/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 fc6b5b69e..0f8152c5e 100644
--- a/Riot/Modules/People/PeopleViewController.m
+++ b/Riot/Modules/People/PeopleViewController.m
@@ -105,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/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 93cabe850..50f0871d5 100644
--- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
+++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
@@ -630,7 +630,7 @@
}
// Check whether the option Ignore may be presented
- if (RiotSettings.shared.roomMemberScreenShowIgnore && self.mxRoomMember.membership == MXMembershipJoin)
+ if (RiotSettings.shared.roomMemberScreenShowIgnore)
{
// is he already ignored ?
if (![self.mainSession isUserIgnored:self.mxRoomMember.userId])
diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift
index 84fd3ab62..c3799610a 100644
--- a/Riot/Modules/Room/RoomCoordinator.swift
+++ b/Riot/Modules/Room/RoomCoordinator.swift
@@ -459,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/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h
index de52c4978..1dcb69ece 100644
--- a/Riot/Modules/Room/RoomViewController.h
+++ b/Riot/Modules/Room/RoomViewController.h
@@ -277,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 12fb57ec8..c63bbb11a 100644
--- a/Riot/Modules/Room/RoomViewController.m
+++ b/Riot/Modules/Room/RoomViewController.m
@@ -89,6 +89,10 @@
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 () 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
@@ -7323,5 +7394,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[self stopActivityIndicator];
}
-@end
+#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/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/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 d1b013f97..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)
}
}
diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift
index 276edf745..8cd8cc824 100644
--- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift
+++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift
@@ -474,6 +474,13 @@ extension ExploreRoomCoordinator: RoomViewControllerDelegate {
}
+ func roomViewControllerDidTapLiveLocationSharingBanner(_ roomViewController: RoomViewController) {
+ // TODO:
+ }
+
+ func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController) {
+ // TODO:
+ }
}
// MARK: - ContactsPickerCoordinatorDelegate
diff --git a/RiotShareExtension/Shared/ShareDataSource.h b/RiotShareExtension/Shared/ShareDataSource.h
index 9dc36d5f5..92aef0c1d 100644
--- a/RiotShareExtension/Shared/ShareDataSource.h
+++ b/RiotShareExtension/Shared/ShareDataSource.h
@@ -31,7 +31,7 @@
@property (nonatomic, strong, readonly) NSSet *selectedRoomIdentifiers;
- (instancetype)initWithFileStore:(MXFileStore *)fileStore
- credentials:(MXCredentials *)credentials;
+ session:(MXSession *)session;
- (void)selectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated;
diff --git a/RiotShareExtension/Shared/ShareDataSource.m b/RiotShareExtension/Shared/ShareDataSource.m
index 83889c333..e097b5da7 100644
--- a/RiotShareExtension/Shared/ShareDataSource.m
+++ b/RiotShareExtension/Shared/ShareDataSource.m
@@ -20,7 +20,7 @@
@interface ShareDataSource ()
@property (nonatomic, strong, readonly) MXFileStore *fileStore;
-@property (nonatomic, strong, readonly) MXCredentials *credentials;
+@property (nonatomic, strong, readonly) MXSession *session;
@property NSArray *recentCellDatas;
@property NSMutableArray *visibleRoomCellDatas;
@@ -32,12 +32,12 @@
@implementation ShareDataSource
- (instancetype)initWithFileStore:(MXFileStore *)fileStore
- credentials:(MXCredentials *)credentials
+ session:(MXSession *)session
{
if (self = [super init])
{
_fileStore = fileStore;
- _credentials = credentials;
+ _session = session;
_internalSelectedRoomIdentifiers = [NSMutableSet set];
@@ -81,19 +81,13 @@
NSMutableArray *cellData = [NSMutableArray array];
- MXRestClient *mxRestClient = [[MXRestClient alloc] initWithCredentials:self.credentials andOnUnrecognizedCertificateBlock:nil andPersistentTokenDataHandler:^(void (^handler)(NSArray *credentials, void (^completion)(BOOL didUpdateCredentials))) {
- [[MXKAccountManager sharedManager] readAndWriteCredentials:handler];
- } andUnauthenticatedHandler:nil];
- // Add a fake matrix session to each room summary to provide it a REST client (used to handle correctly the room avatar).
- MXSession *session = [[MXSession alloc] initWithMatrixRestClient:mxRestClient];
-
for (id summary in summaries)
{
if (!summary.hiddenFromUser && summary.roomType == MXRoomTypeRoom)
{
if ([summary respondsToSelector:@selector(setMatrixSession:)])
{
- [summary setMatrixSession:session];
+ [summary setMatrixSession:self.session];
}
MXKRecentCellData *recentCellData = [[MXKRecentCellData alloc] initWithRoomSummary:summary dataSource:nil];
diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m
index ad7a75ad3..2233b352d 100644
--- a/RiotShareExtension/Shared/ShareManager.m
+++ b/RiotShareExtension/Shared/ShareManager.m
@@ -34,11 +34,21 @@
@property (nonatomic, strong) MXKAccount *userAccount;
@property (nonatomic, strong) MXFileStore *fileStore;
+/**
+ An array of rooms that the item is being shared to. This is to maintain a strong ref
+ to all necessary `MXRoom`s until sharing has completed.
+ */
+@property (nonatomic, strong) NSMutableArray *selectedRooms;
+
@end
@implementation ShareManager
+/// A fake matrix session used to provide summaries with a REST client to handle room avatars.
+/// The session is stored statically to prevent new ones from being created for each share.
+static MXSession *fakeSession;
+
- (instancetype)initWithShareItemSender:(id)itemSender
type:(ShareManagerType)type
{
@@ -94,17 +104,19 @@
session.crypto.warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now
- NSMutableArray *rooms = [NSMutableArray array];
+ self.selectedRooms = [NSMutableArray array];
for (NSString *roomIdentifier in roomIdentifiers) {
MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session];
if (room) {
- [rooms addObject:room];
+ [self.selectedRooms addObject:room];
}
}
- [self.shareItemSender sendItemsToRooms:rooms success:^{
+ [self.shareItemSender sendItemsToRooms:self.selectedRooms success:^{
+ self.selectedRooms = nil;
self.completionCallback(ShareManagerResultFinished);
} failure:^(NSArray *errors) {
+ self.selectedRooms = nil;
[self showFailureAlert:[VectorL10n roomEventFailedToSend]];
}];
@@ -174,6 +186,7 @@
// We consider the first enabled account.
// TODO: Handle multiple accounts
self.userAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject;
+ [self checkFakeSession];
}
// Reset the file store to reload the room data.
@@ -183,12 +196,12 @@
_fileStore = nil;
}
- if (self.userAccount)
+ if (self.userAccount && fakeSession)
{
_fileStore = [[MXFileStore alloc] initWithCredentials:self.userAccount.mxCredentials];
ShareDataSource *roomDataSource = [[ShareDataSource alloc] initWithFileStore:_fileStore
- credentials:self.userAccount.mxCredentials];
+ session:fakeSession];
[self.shareViewController configureWithState:ShareViewControllerAccountStateConfigured
roomDataSource:roomDataSource];
@@ -198,6 +211,27 @@
}
}
+- (void)checkFakeSession
+{
+ if (!self.userAccount)
+ {
+ return;
+ }
+
+ if (fakeSession && [fakeSession.credentials.userId isEqualToString:self.userAccount.mxCredentials.userId])
+ {
+ return;
+ }
+
+ MXRestClient *mxRestClient = [[MXRestClient alloc] initWithCredentials:self.userAccount.mxCredentials
+ andOnUnrecognizedCertificateBlock:nil
+ andPersistentTokenDataHandler:^(void (^handler)(NSArray *credentials, void (^completion)(BOOL didUpdateCredentials))) {
+ [[MXKAccountManager sharedManager] readAndWriteCredentials:handler];
+ } andUnauthenticatedHandler:nil];
+
+ fakeSession = [[MXSession alloc] initWithMatrixRestClient:mxRestClient];
+}
+
- (void)didStartSending
{
[self.shareViewController showProgressIndicator];
diff --git a/RiotShareExtension/Sources/ShareItemSender.m b/RiotShareExtension/Sources/ShareItemSender.m
index 1758622ec..2c11e2a46 100644
--- a/RiotShareExtension/Sources/ShareItemSender.m
+++ b/RiotShareExtension/Sources/ShareItemSender.m
@@ -35,7 +35,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
@interface ShareItemSender ()
-@property (nonatomic, strong, readonly) UIViewController *rootViewController;
+@property (nonatomic, weak, readonly) UIViewController *rootViewController;
@property (nonatomic, strong, readonly) ShareExtensionShareItemProvider *shareItemProvider;
@property (nonatomic, strong, readonly) NSMutableArray *pendingImages;
@@ -641,7 +641,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
{
if (!RiotSettings.shared.showMediaCompressionPrompt)
{
- [MXSDKOptions sharedInstance].videoConversionPresetName = AVCaptureSessionPreset1920x1080;
+ [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080;
sendVideo();
}
else
diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift
index 816498006..96c917259 100644
--- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift
+++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift
@@ -71,13 +71,14 @@ struct AnalyticsPrompt: View {
Text(VectorL10n.analyticsPromptTitle(AppInfo.current.displayName))
.font(theme.fonts.title2B)
+ .multilineTextAlignment(.center)
.foregroundColor(theme.colors.primaryContent)
.padding(.bottom, 2)
messageText
.font(theme.fonts.body)
- .foregroundColor(theme.colors.secondaryContent)
.multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.secondaryContent)
Divider()
.background(theme.colors.quinaryContent)
@@ -117,8 +118,11 @@ struct AnalyticsPrompt: View {
.padding(.top, 50)
.padding(.horizontal, horizontalPadding)
}
+ .frame(maxWidth: OnboardingConstants.maxContentWidth)
+ .frame(maxWidth: .infinity)
buttons
+ .frame(maxWidth: OnboardingConstants.maxContentWidth)
.padding(.horizontal, horizontalPadding)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
index ccc315a73..5bed53bd5 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
@@ -35,22 +35,15 @@ struct AvatarImage: View {
case .empty:
ProgressView()
case .placeholder(let firstCharacter, let colorIndex):
- Text(firstCharacter)
- .padding(4)
- .frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
- .foregroundColor(.white)
- .background(theme.colors.namesAndAvatars[colorIndex])
- .clipShape(Circle())
- // Make the text resizable (i.e. Make it large and then allow it to scale down)
- .font(.system(size: 200))
- .minimumScaleFactor(0.001)
+ PlaceholderAvatarImage(firstCharacter: firstCharacter,
+ colorIndex: colorIndex)
case .avatar(let image):
Image(uiImage: image)
.resizable()
- .frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
- .clipShape(Circle())
}
}
+ .frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
+ .clipShape(Circle())
.onAppear {
viewModel.inject(dependencies: dependencies)
viewModel.loadAvatar(
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift
new file mode 100644
index 000000000..93f0a7186
--- /dev/null
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift
@@ -0,0 +1,60 @@
+//
+// 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 SwiftUI
+
+@available(iOS 14.0, *)
+/// A reusable view that will show a standard placeholder avatar with the
+/// supplied character and colour index for the `namesAndAvatars` color array.
+///
+/// This view has a forced 1:1 aspect ratio but will appear very large until a `.frame`
+/// modifier is applied.
+struct PlaceholderAvatarImage: View {
+
+ // MARK: - Private
+
+ @Environment(\.theme) private var theme
+
+ // MARK: - Public
+
+ let firstCharacter: Character
+ let colorIndex: Int
+
+ // MARK: - Views
+
+ var body: some View {
+ ZStack {
+ theme.colors.namesAndAvatars[colorIndex]
+
+ Text(String(firstCharacter))
+ .padding(4)
+ .foregroundColor(.white)
+ // Make the text resizable (i.e. Make it large and then allow it to scale down)
+ .font(.system(size: 200))
+ .minimumScaleFactor(0.001)
+ }
+ .aspectRatio(1, contentMode: .fill)
+ }
+}
+
+@available(iOS 14.0, *)
+struct Previews_TemplateAvatarImage_Previews: PreviewProvider {
+ static var previews: some View {
+ PlaceholderAvatarImage(firstCharacter: "X", colorIndex: 1)
+ .clipShape(Circle())
+ .frame(width: 150, height: 100)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
index 5908583b0..e0bb01e97 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
@@ -35,7 +35,7 @@ struct SpaceAvatarImage: View {
case .empty:
ProgressView()
case .placeholder(let firstCharacter, let colorIndex):
- Text(firstCharacter)
+ Text(String(firstCharacter))
.padding(10)
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
.foregroundColor(.white)
diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift
index 1a5f8f032..2b49dc289 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift
@@ -42,10 +42,11 @@ class AvatarViewModel: InjectableObject, ObservableObject {
colorCount: Int,
avatarSize: AvatarSize) {
- self.viewState = .placeholder(
- firstCharacterCapitalized(displayName),
- stableColorIndex(matrixItemId: matrixItemId, colorCount: colorCount)
- )
+ let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName,
+ matrixItemId: matrixItemId,
+ colorCount: colorCount)
+
+ self.viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex)
guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else {
return
@@ -60,31 +61,4 @@ class AvatarViewModel: InjectableObject, ObservableObject {
}
.store(in: &cancellables)
}
-
- /// Get the first character of a string capialized or else an empty string.
- /// - Parameter string: The input string to get the capitalized letter from.
- /// - Returns: The capitalized first letter.
- private func firstCharacterCapitalized(_ string: String?) -> String {
- guard let character = string?.first else {
- return ""
- }
- return String(character).capitalized
- }
-
- /// Provides the same color each time for a specified matrixId
- ///
- /// Same algorithm as in AvatarGenerator.
- /// - Parameters:
- /// - matrixItemId: the matrix id used as input to create the stable index.
- /// - colorCount: The number of total colors we want to index in to.
- /// - Returns: The stable index.
- private func stableColorIndex(matrixItemId: String, colorCount: Int) -> Int {
- // Sum all characters
- let sum = matrixItemId.utf8
- .map({ UInt($0) })
- .reduce(0, +)
- // modulo the color count
- return Int(sum) % colorCount
- }
-
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift
index 203d3f9e9..cac2a70d4 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift
@@ -19,6 +19,6 @@ import UIKit
enum AvatarViewState {
case empty
- case placeholder(String, Int)
+ case placeholder(Character, Int)
case avatar(UIImage)
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/PlaceholderAvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/PlaceholderAvatarViewModel.swift
new file mode 100644
index 000000000..d5c131b39
--- /dev/null
+++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/PlaceholderAvatarViewModel.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
+
+/// Simple view model that computes the placeholder avatar properties.
+struct PlaceholderAvatarViewModel {
+ /// The displayname used to create the `firstCharacterCapitalized`.
+ let displayName: String?
+ /// The matrix id used as input to create the `stableColorIndex` from.
+ let matrixItemId: String
+ /// The number of total colors available for the `stableColorIndex`.
+ let colorCount: Int
+
+ /// Get the first character of the display name capitalized or else a space character.
+ var firstCharacterCapitalized: Character {
+ return displayName?.capitalized.first ?? " "
+ }
+
+ /// Provides the same color each time for a specified matrixId
+ ///
+ /// Same algorithm as in AvatarGenerator.
+ /// - Parameters:
+ /// - matrixItemId: the matrix id used as input to create the stable index.
+ /// - Returns: The stable index.
+ var stableColorIndex: Int {
+ // Sum all characters
+ let sum = matrixItemId.utf8
+ .map({ UInt($0) })
+ .reduce(0, +)
+ // modulo the color count
+ return Int(sum) % colorCount
+ }
+}
diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
index 3c5909a4c..7201176c3 100644
--- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
+++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
@@ -20,6 +20,8 @@ import Foundation
@available(iOS 14.0, *)
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
+ MockOnboardingAvatarScreenState.self,
+ MockOnboardingDisplayNameScreenState.self,
MockOnboardingCongratulationsScreenState.self,
MockOnboardingUseCaseSelectionScreenState.self,
MockOnboardingSplashScreenScreenState.self,
diff --git a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift
index 5824cbc85..b2d0206fc 100644
--- a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift
+++ b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift
@@ -23,29 +23,34 @@ struct PrimaryActionButtonStyle: ButtonStyle {
var customColor: Color? = nil
+ private var fontColor: Color {
+ // Always white unless disabled with a dark theme.
+ .white.opacity(theme.isDark && !isEnabled ? 0.3 : 1.0)
+ }
+
+ private var backgroundColor: Color {
+ customColor ?? theme.colors.accent
+ }
+
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(12.0)
.frame(maxWidth: .infinity)
- .foregroundColor(.white)
+ .foregroundColor(fontColor)
.font(theme.fonts.body)
- .background(backgroundColor(configuration.isPressed))
- .opacity(isEnabled ? 1.0 : 0.6)
+ .background(backgroundColor.opacity(backgroundOpacity(when: configuration.isPressed)))
.cornerRadius(8.0)
}
- func backgroundColor(_ isPressed: Bool) -> Color {
- if let customColor = customColor {
- return customColor
- }
-
- return isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent
+ func backgroundOpacity(when isPressed: Bool) -> CGFloat {
+ guard isEnabled else { return 0.3 }
+ return isPressed ? 0.6 : 1.0
}
}
@available(iOS 14.0, *)
struct PrimaryActionButtonStyle_Previews: PreviewProvider {
- static var previews: some View {
+ static var buttons: some View {
Group {
VStack {
Button("Enabled") { }
@@ -67,4 +72,11 @@ struct PrimaryActionButtonStyle_Previews: PreviewProvider {
.padding()
}
}
+
+ static var previews: some View {
+ buttons
+ .theme(.light).preferredColorScheme(.light)
+ buttons
+ .theme(.dark).preferredColorScheme(.dark)
+ }
}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift
new file mode 100644
index 000000000..70a9651d2
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift
@@ -0,0 +1,198 @@
+//
+// 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 SwiftUI
+import CommonKit
+
+struct OnboardingAvatarCoordinatorParameters {
+ let userSession: UserSession
+}
+
+@available(iOS 14.0, *)
+final class OnboardingAvatarCoordinator: Coordinator, Presentable {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: OnboardingAvatarCoordinatorParameters
+ private let onboardingAvatarHostingController: VectorHostingController
+ private var onboardingAvatarViewModel: OnboardingAvatarViewModelProtocol
+
+ private var indicatorPresenter: UserIndicatorTypePresenterProtocol
+ private var waitingIndicator: UserIndicator?
+
+ private lazy var cameraPresenter: CameraPresenter = {
+ let presenter = CameraPresenter()
+ presenter.delegate = self
+ return presenter
+ }()
+
+ private lazy var mediaPickerPresenter: MediaPickerPresenter = {
+ let presenter = MediaPickerPresenter()
+ presenter.delegate = self
+ return presenter
+ }()
+
+ private lazy var mediaUploader: MXMediaLoader = MXMediaManager.prepareUploader(withMatrixSession: parameters.userSession.matrixSession,
+ initialRange: 0,
+ andRange: 1.0)
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+ var completion: ((UserSession) -> Void)?
+
+ // MARK: - Setup
+
+ init(parameters: OnboardingAvatarCoordinatorParameters) {
+ self.parameters = parameters
+ let viewModel = OnboardingAvatarViewModel(userId: parameters.userSession.userId,
+ displayName: parameters.userSession.account.userDisplayName,
+ avatarColorCount: DefaultThemeSwiftUI().colors.namesAndAvatars.count)
+ let view = OnboardingAvatarScreen(viewModel: viewModel.context)
+ onboardingAvatarViewModel = viewModel
+ onboardingAvatarHostingController = VectorHostingController(rootView: view)
+ onboardingAvatarHostingController.vc_removeBackTitle()
+ onboardingAvatarHostingController.enableNavigationBarScrollEdgeAppearance = true
+
+ indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: onboardingAvatarHostingController)
+ }
+
+
+ // MARK: - Public
+
+ func start() {
+ MXLog.debug("[OnboardingAvatarCoordinator] did start.")
+ onboardingAvatarViewModel.completion = { [weak self] result in
+ guard let self = self else { return }
+ MXLog.debug("[OnboardingAvatarCoordinator] OnboardingAvatarViewModel did complete with result: \(result).")
+ switch result {
+ case .pickImage:
+ self.pickImage()
+ case .takePhoto:
+ self.takePhoto()
+ case .save(let avatar):
+ self.setAvatar(avatar)
+ case .skip:
+ self.completion?(self.parameters.userSession)
+ }
+ }
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.onboardingAvatarHostingController
+ }
+
+ // MARK: - Private
+
+ /// Show a blocking activity indicator whilst saving.
+ private func startWaiting() {
+ waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.saving, isInteractionBlocking: true))
+ }
+
+ /// Hide the currently displayed activity indicator.
+ private func stopWaiting() {
+ waitingIndicator = nil
+ }
+
+ /// Present an image picker for the device photo library.
+ private func pickImage() {
+ let controller = toPresentable()
+ mediaPickerPresenter.presentPicker(from: controller, with: .images, animated: true)
+ }
+
+ /// Present a camera view to take a photo to use for the avatar.
+ private func takePhoto() {
+ let controller = toPresentable()
+ cameraPresenter.presentCamera(from: controller, with: [.image], animated: true)
+ }
+
+ /// Set the supplied image as user's avatar, completing the screen's display if successful.
+ func setAvatar(_ image: UIImage?) {
+ guard let image = image else {
+ MXLog.error("[OnboardingAvatarCoordinator] setAvatar called with a nil image.")
+ return
+ }
+
+ startWaiting()
+
+ guard let avatarData = MXKTools.forceImageOrientationUp(image)?.jpegData(compressionQuality: 0.5) else {
+ MXLog.error("[OnboardingAvatarCoordinator] Failed to create jpeg data.")
+ self.stopWaiting()
+ self.onboardingAvatarViewModel.processError(nil)
+ return
+ }
+
+ mediaUploader.uploadData(avatarData, filename: nil, mimeType: "image/jpeg") { [weak self] urlString in
+ guard let self = self else { return }
+
+ guard let urlString = urlString else {
+ MXLog.error("[OnboardingAvatarCoordinator] Missing URL string for avatar.")
+ self.stopWaiting()
+ self.onboardingAvatarViewModel.processError(nil)
+ return
+ }
+
+ self.parameters.userSession.account.setUserAvatarUrl(urlString) { [weak self] in
+ guard let self = self else { return }
+ self.stopWaiting()
+ self.completion?(self.parameters.userSession)
+ } failure: { [weak self] error in
+ guard let self = self else { return }
+ self.stopWaiting()
+ self.onboardingAvatarViewModel.processError(error as NSError?)
+ }
+ } failure: { [weak self] error in
+ guard let self = self else { return }
+ self.stopWaiting()
+ self.onboardingAvatarViewModel.processError(error as NSError?)
+ }
+ }
+}
+
+// MARK: - MediaPickerPresenterDelegate
+
+@available(iOS 14.0, *)
+extension OnboardingAvatarCoordinator: MediaPickerPresenterDelegate {
+ func mediaPickerPresenter(_ presenter: MediaPickerPresenter, didPickImage image: UIImage) {
+ onboardingAvatarViewModel.updateAvatarImage(with: image)
+ presenter.dismiss(animated: true, completion: nil)
+ }
+
+ func mediaPickerPresenterDidCancel(_ presenter: MediaPickerPresenter) {
+ presenter.dismiss(animated: true, completion: nil)
+ }
+}
+
+// MARK: - CameraPresenterDelegate
+
+@available(iOS 14.0, *)
+extension OnboardingAvatarCoordinator: CameraPresenterDelegate {
+ func cameraPresenter(_ presenter: CameraPresenter, didSelectImage image: UIImage) {
+ onboardingAvatarViewModel.updateAvatarImage(with: image)
+ presenter.dismiss(animated: true, completion: nil)
+ }
+
+ func cameraPresenter(_ presenter: CameraPresenter, didSelectVideoAt url: URL) {
+ presenter.dismiss(animated: true, completion: nil)
+ }
+
+ func cameraPresenterDidCancel(_ presenter: CameraPresenter) {
+ presenter.dismiss(animated: true, completion: nil)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
new file mode 100644
index 000000000..8dff96b67
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
@@ -0,0 +1,77 @@
+//
+// 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 SwiftUI
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+@available(iOS 14.0, *)
+enum MockOnboardingAvatarScreenState: MockScreenState, CaseIterable {
+ // A case for each state you want to represent
+ // with specific, minimal associated data that will allow you
+ // mock that screen.
+ case placeholderAvatar(userId: String, displayName: String)
+ case userSelectedAvatar(userId: String, displayName: String)
+
+ /// The associated screen
+ var screenType: Any.Type {
+ OnboardingAvatarScreen.self
+ }
+
+ /// A list of screen state definitions
+ static var allCases: [MockOnboardingAvatarScreenState] {
+ let userId = "@example:matrix.org"
+ let displayName = "Jane"
+
+ return [
+ .placeholderAvatar(userId: userId, displayName: displayName),
+ .userSelectedAvatar(userId: userId, displayName: displayName)
+ ]
+ }
+
+ /// Generate the view struct for the screen state.
+ var screenView: ([Any], AnyView) {
+ let avatarColorCount = DefaultThemeSwiftUI().colors.namesAndAvatars.count
+ let viewModel: OnboardingAvatarViewModel
+ switch self {
+ case .placeholderAvatar(let userId, let displayName):
+ viewModel = OnboardingAvatarViewModel(userId: userId, displayName: displayName, avatarColorCount: avatarColorCount)
+ case .userSelectedAvatar(let userId, let displayName):
+ viewModel = OnboardingAvatarViewModel(userId: userId, displayName: displayName, avatarColorCount: avatarColorCount)
+ viewModel.updateAvatarImage(with: Asset.Images.appSymbol.image)
+ }
+
+ return (
+ [self, viewModel],
+ AnyView(OnboardingAvatarScreen(viewModel: viewModel.context)
+ .addDependency(MockAvatarService.example))
+ )
+ }
+}
+
+@available(iOS 14.0, *)
+extension MockOnboardingAvatarScreenState: CustomStringConvertible {
+ // Added to have different descriptions in the SwiftUI target's list.
+ var description: String {
+ switch self {
+ case .placeholderAvatar:
+ return "placeholderAvatar"
+ case .userSelectedAvatar:
+ return "userSelectedAvatar"
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarModels.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarModels.swift
new file mode 100644
index 000000000..7f0187ebb
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarModels.swift
@@ -0,0 +1,63 @@
+//
+// 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
+
+// MARK: View model
+
+enum OnboardingAvatarViewModelResult {
+ /// The user would like to choose an image from their photo library.
+ case pickImage
+ /// The user would like to take a photo to use as their avatar.
+ case takePhoto
+ /// The user would like to set specified image as their avatar.
+ case save(UIImage?)
+ /// Move on to the next screen in the flow without setting an avatar.
+ case skip
+}
+
+// MARK: View
+
+struct OnboardingAvatarViewState: BindableState {
+ /// The letter shown in the placeholder avatar.
+ let placeholderAvatarLetter: Character
+ /// The color index to use for the placeholder avatar's background.
+ let placeholderAvatarColorIndex: Int
+ /// The image selected by the user to use as their avatar.
+ var avatar: UIImage?
+ var bindings: OnboardingAvatarBindings
+
+ /// The image shown in the avatar's button.
+ var buttonImage: ImageAsset {
+ avatar == nil ? Asset.Images.onboardingAvatarCamera : Asset.Images.onboardingAvatarEdit
+ }
+}
+
+struct OnboardingAvatarBindings {
+ /// The currently displayed alert's info value otherwise `nil`.
+ var alertInfo: AlertInfo?
+}
+
+enum OnboardingAvatarViewAction {
+ /// The user would like to choose an image from their photo library.
+ case pickImage
+ /// The user would like to take a photo to use as their avatar.
+ case takePhoto
+ /// The user would like to save their chosen avatar image.
+ case save
+ /// Move on to the next screen in the flow without setting an avatar.
+ case skip
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift
new file mode 100644
index 000000000..72376b34d
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift
@@ -0,0 +1,67 @@
+//
+// 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 SwiftUI
+import Combine
+
+@available(iOS 14, *)
+typealias OnboardingAvatarViewModelType = StateStoreViewModel
+@available(iOS 14, *)
+class OnboardingAvatarViewModel: OnboardingAvatarViewModelType, OnboardingAvatarViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ // MARK: Public
+
+ var completion: ((OnboardingAvatarViewModelResult) -> Void)?
+
+ // MARK: - Setup
+
+ init(userId: String, displayName: String?, avatarColorCount: Int) {
+ let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName, matrixItemId: userId, colorCount: avatarColorCount)
+ let initialViewState = OnboardingAvatarViewState(placeholderAvatarLetter: placeholderViewModel.firstCharacterCapitalized,
+ placeholderAvatarColorIndex: placeholderViewModel.stableColorIndex,
+ bindings: OnboardingAvatarBindings())
+ super.init(initialViewState: initialViewState)
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: OnboardingAvatarViewAction) {
+ switch viewAction {
+ case .pickImage:
+ completion?(.pickImage)
+ case .takePhoto:
+ completion?(.takePhoto)
+ case .save:
+ completion?(.save(state.avatar))
+ case .skip:
+ completion?(.skip)
+ }
+ }
+
+ func updateAvatarImage(with image: UIImage?) {
+ state.avatar = image
+ }
+
+ func processError(_ error: NSError?) {
+ state.bindings.alertInfo = AlertInfo(error: error)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift
new file mode 100644
index 000000000..0e757a9dd
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift
@@ -0,0 +1,31 @@
+//
+// 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 SwiftUI
+
+protocol OnboardingAvatarViewModelProtocol {
+
+ var completion: ((OnboardingAvatarViewModelResult) -> Void)? { get set }
+ @available(iOS 14, *)
+ var context: OnboardingAvatarViewModelType.Context { get }
+
+ /// Update the view model to show the image that the user has picked.
+ func updateAvatarImage(with image: UIImage?)
+
+ /// Update the view model to show that an error has occurred.
+ /// - Parameter error: The error to be displayed or `nil` to display a generic alert.
+ func processError(_ error: NSError?)
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift
new file mode 100644
index 000000000..f9cc94e70
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift
@@ -0,0 +1,70 @@
+//
+// 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 XCTest
+import RiotSwiftUI
+
+@available(iOS 14.0, *)
+class OnboardingAvatarUITests: MockScreenTest {
+
+ override class var screenType: MockScreenState.Type {
+ return MockOnboardingAvatarScreenState.self
+ }
+
+ override class func createTest() -> MockScreenTest {
+ return OnboardingAvatarUITests(selector: #selector(verifyOnboardingAvatarScreen))
+ }
+
+ func verifyOnboardingAvatarScreen() throws {
+ guard let screenState = screenState as? MockOnboardingAvatarScreenState else { fatalError("no screen") }
+ switch screenState {
+ case .placeholderAvatar(let userId, let displayName):
+ verifyPlaceholderAvatar(userId: userId, displayName: displayName)
+ case .userSelectedAvatar:
+ verifyUserSelectedAvatar()
+ }
+ }
+
+ func verifyPlaceholderAvatar(userId: String, displayName: String) {
+ guard let firstLetter = displayName.uppercased().first else {
+ XCTFail("Unable to get the first letter of the display name.")
+ return
+ }
+
+ let placeholderAvatar = app.staticTexts["placeholderAvatar"]
+ XCTAssertTrue(placeholderAvatar.exists, "The placeholder avatar should be shown.")
+ XCTAssertEqual(placeholderAvatar.label, String(firstLetter), "The placeholder avatar should show the first letter of the display name.")
+
+ let avatarImage = app.images["avatarImage"]
+ XCTAssertFalse(avatarImage.exists, "The avatar image should be hidden as no selection has been made.")
+
+ let saveButton = app.buttons["saveButton"]
+ XCTAssertTrue(saveButton.exists, "There should be a save button.")
+ XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.")
+ }
+
+ func verifyUserSelectedAvatar() {
+ let placeholderAvatar = app.otherElements["placeholderAvatar"]
+ XCTAssertFalse(placeholderAvatar.exists, "The placeholder avatar should be hidden.")
+
+ let avatarImage = app.images["avatarImage"]
+ XCTAssertTrue(avatarImage.exists, "The selected avatar should be shown.")
+
+ let saveButton = app.buttons["saveButton"]
+ XCTAssertTrue(saveButton.exists, "There should be a save button.")
+ XCTAssertTrue(saveButton.isEnabled, "The save button should be enabled.")
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift
new file mode 100644
index 000000000..a65df871e
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.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 XCTest
+import Combine
+
+@testable import RiotSwiftUI
+
+@available(iOS 14.0, *)
+class OnboardingAvatarViewModelTests: XCTestCase {
+ private enum Constants {
+ static let userId = "@user:matrix.org"
+ static let displayName = "Alice"
+ static let avatarColorCount = DefaultThemeSwiftUI().colors.namesAndAvatars.count
+ static let avatarImage = Asset.Images.appSymbol.image
+ }
+
+ var viewModel: OnboardingAvatarViewModelProtocol!
+ var context: OnboardingAvatarViewModelType.Context!
+
+ override func setUpWithError() throws {
+ viewModel = OnboardingAvatarViewModel(userId: Constants.userId,
+ displayName: Constants.displayName,
+ avatarColorCount: Constants.avatarColorCount)
+ context = viewModel.context
+ }
+
+ func testInitialState() {
+ XCTAssertEqual(context.viewState.placeholderAvatarLetter, "A")
+ XCTAssertNil(context.viewState.avatar)
+ XCTAssertNil(context.viewState.bindings.alertInfo)
+ }
+
+ func testUpdatingAvatar() {
+ // Given the default view model
+ XCTAssertNil(context.viewState.avatar, "The default view state should not have an avatar.")
+
+ // When updating the image
+ viewModel.updateAvatarImage(with: Constants.avatarImage)
+
+ // Then the view state should contain the new image
+ XCTAssertEqual(context.viewState.avatar, Constants.avatarImage, "The view state should contain the new avatar image.")
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift
new file mode 100644
index 000000000..7e611900a
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift
@@ -0,0 +1,157 @@
+//
+// 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 SwiftUI
+import DesignKit
+
+@available(iOS 14.0, *)
+struct OnboardingAvatarScreen: View {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme
+
+ @State private var isPresentingPickerSelection = false
+
+ // MARK: Public
+
+ @ObservedObject var viewModel: OnboardingAvatarViewModel.Context
+
+ var body: some View {
+ ScrollView {
+ VStack(spacing: 0) {
+ avatar
+ .padding(.horizontal, 2)
+ .padding(.bottom, 40)
+
+ header
+ .padding(.bottom, 40)
+
+ buttons
+ }
+ .padding(.horizontal)
+ .padding(.top, 8)
+ .frame(maxWidth: OnboardingConstants.maxContentWidth)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .accentColor(theme.colors.accent)
+ .background(theme.colors.background.ignoresSafeArea())
+ .alert(item: $viewModel.alertInfo) { $0.alert }
+ }
+
+
+ /// The user's avatar along with a picker button
+ var avatar: some View {
+ Group {
+ if let avatarImage = viewModel.viewState.avatar {
+ Image(uiImage: avatarImage)
+ .resizable()
+ .scaledToFill()
+ .accessibilityIdentifier("avatarImage")
+ } else {
+ PlaceholderAvatarImage(firstCharacter: viewModel.viewState.placeholderAvatarLetter,
+ colorIndex: viewModel.viewState.placeholderAvatarColorIndex)
+ .accessibilityIdentifier("placeholderAvatar")
+ }
+ }
+ .clipShape(Circle())
+ .frame(width: 120, height: 120)
+ .overlay(cameraButton, alignment: .bottomTrailing)
+ .onTapGesture { isPresentingPickerSelection = true }
+ .actionSheet(isPresented: $isPresentingPickerSelection) { pickerSelectionActionSheet }
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel(VectorL10n.onboardingAvatarAccessibilityLabel)
+ .accessibilityValue(VectorL10n.edit)
+ }
+
+ /// The button to indicate the user can tap to select an avatar
+ /// Note: The whole avatar is tappable to make this easier.
+ var cameraButton: some View {
+ ZStack {
+ Circle()
+ .foregroundColor(theme.colors.background)
+ .shadow(color: .black.opacity(0.15), radius: 2.4, y: 2.4)
+
+ Image(viewModel.viewState.buttonImage.name)
+ .renderingMode(.template)
+ .foregroundColor(theme.colors.secondaryContent)
+ }
+ .frame(width: 40, height: 40)
+ }
+
+ /// The action sheet that asks how the user would like to set their avatar.
+ var pickerSelectionActionSheet: ActionSheet {
+ ActionSheet(title: Text(VectorL10n.onboardingAvatarTitle), buttons: [
+ .default(Text(VectorL10n.imagePickerActionCamera)) {
+ viewModel.send(viewAction: .takePhoto)
+ },
+ .default(Text(VectorL10n.imagePickerActionLibrary)) {
+ viewModel.send(viewAction: .pickImage)
+ },
+ .cancel()
+ ])
+ }
+
+ /// The screen's title and message views.
+ var header: some View {
+ VStack(spacing: 8) {
+ Text(VectorL10n.onboardingAvatarTitle)
+ .font(theme.fonts.title2B)
+ .multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.primaryContent)
+
+ Text(VectorL10n.onboardingAvatarMessage)
+ .font(theme.fonts.subheadline)
+ .multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.secondaryContent)
+ }
+ }
+
+ /// The main action buttons in the form.
+ var buttons: some View {
+ VStack(spacing: 8) {
+ Button(VectorL10n.onboardingPersonalizationSave) {
+ viewModel.send(viewAction: .save)
+ }
+ .buttonStyle(PrimaryActionButtonStyle())
+ .disabled(viewModel.viewState.avatar == nil)
+ .accessibilityIdentifier("saveButton")
+
+ Button { viewModel.send(viewAction: .skip) } label: {
+ Text(VectorL10n.onboardingPersonalizationSkip)
+ .font(theme.fonts.body)
+ .padding(12)
+ }
+ }
+ }
+}
+
+// MARK: - Previews
+
+@available(iOS 15.0, *)
+struct OnboardingAvatar_Previews: PreviewProvider {
+ static let stateRenderer = MockOnboardingAvatarScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup(addNavigation: true)
+ .navigationViewStyle(.stack)
+ .theme(.light).preferredColorScheme(.light)
+ stateRenderer.screenGroup(addNavigation: true)
+ .navigationViewStyle(.stack)
+ .theme(.dark).preferredColorScheme(.dark)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift
index 6bed87cc2..47f6ee54b 100644
--- a/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift
@@ -17,7 +17,18 @@
import SwiftUI
struct OnboardingCongratulationsCoordinatorParameters {
- let userId: String
+ /// The user session used to determine the user ID to display.
+ let userSession: UserSession
+ /// When `true` the "Personalise Profile" button will be hidden, preventing the
+ /// user from setting a displayname or avatar.
+ let personalizationDisabled: Bool
+}
+
+enum OnboardingCongratulationsCoordinatorResult {
+ /// Show the display name and/or avatar screens for the user to personalize their profile.
+ case personalizeProfile(UserSession)
+ /// Continue the flow by skipping the display name and avatar screens.
+ case takeMeHome(UserSession)
}
final class OnboardingCongratulationsCoordinator: Coordinator, Presentable {
@@ -34,7 +45,7 @@ final class OnboardingCongratulationsCoordinator: Coordinator, Presentable {
// Must be used only internally
var childCoordinators: [Coordinator] = []
- var completion: ((OnboardingCongratulationsViewModelResult) -> Void)?
+ var completion: ((OnboardingCongratulationsCoordinatorResult) -> Void)?
// MARK: - Setup
@@ -42,7 +53,9 @@ final class OnboardingCongratulationsCoordinator: Coordinator, Presentable {
init(parameters: OnboardingCongratulationsCoordinatorParameters) {
self.parameters = parameters
- let viewModel = OnboardingCongratulationsViewModel(userId: parameters.userId)
+ // TODO: Add confetti when personalizationDisabled is false
+ let viewModel = OnboardingCongratulationsViewModel(userId: parameters.userSession.userId,
+ personalizationDisabled: parameters.personalizationDisabled)
let view = OnboardingCongratulationsScreen(viewModel: viewModel.context)
onboardingCongratulationsViewModel = viewModel
onboardingCongratulationsHostingController = VectorHostingController(rootView: view)
@@ -54,7 +67,13 @@ final class OnboardingCongratulationsCoordinator: Coordinator, Presentable {
onboardingCongratulationsViewModel.completion = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[OnboardingCongratulationsCoordinator] OnboardingCongratulationsViewModel did complete with result: \(result).")
- self.completion?(result)
+
+ switch result {
+ case .personalizeProfile:
+ self.completion?(.personalizeProfile(self.parameters.userSession))
+ case .takeMeHome:
+ self.completion?(.takeMeHome(self.parameters.userSession))
+ }
}
}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift
index 4aa617f1c..f2b1773bc 100644
--- a/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift
@@ -24,7 +24,8 @@ enum MockOnboardingCongratulationsScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
- case congratulations
+ case regular
+ case personalizationDisabled
/// The associated screen
var screenType: Any.Type {
@@ -33,14 +34,18 @@ enum MockOnboardingCongratulationsScreenState: MockScreenState, CaseIterable {
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
- let viewModel = OnboardingCongratulationsViewModel(userId: "@testuser:example.com")
+ let viewModel: OnboardingCongratulationsViewModel
- // can simulate service and viewModel actions here if needs be.
+ switch self {
+ case .regular:
+ viewModel = OnboardingCongratulationsViewModel(userId: "@testuser:example.com")
+ case .personalizationDisabled:
+ viewModel = OnboardingCongratulationsViewModel(userId: "@testuser:example.com", personalizationDisabled: true)
+ }
return (
[self, viewModel],
- AnyView(OnboardingCongratulationsScreen(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ AnyView(OnboardingCongratulationsScreen(viewModel: viewModel.context))
)
}
}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift
index d8eb9e926..1a83694dc 100644
--- a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift
@@ -21,14 +21,15 @@ import Foundation
// MARK: View model
enum OnboardingCongratulationsViewModelResult {
- case personaliseProfile
+ case personalizeProfile
case takeMeHome
}
// MARK: View
struct OnboardingCongratulationsViewState: BindableState {
- var userId: String
+ let userId: String
+ let personalizationDisabled: Bool
}
enum OnboardingCongratulationsViewAction {
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift
index 26a7ebfdd..4e7924015 100644
--- a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift
@@ -33,8 +33,9 @@ class OnboardingCongratulationsViewModel: OnboardingCongratulationsViewModelType
// MARK: - Setup
- init(userId: String, initialCount: Int = 0) {
- super.init(initialViewState: OnboardingCongratulationsViewState(userId: userId))
+ init(userId: String, personalizationDisabled: Bool = false) {
+ super.init(initialViewState: OnboardingCongratulationsViewState(userId: userId,
+ personalizationDisabled: personalizationDisabled))
}
// MARK: - Public
@@ -42,7 +43,7 @@ class OnboardingCongratulationsViewModel: OnboardingCongratulationsViewModelType
override func process(viewAction: OnboardingCongratulationsViewAction) {
switch viewAction {
case .personaliseProfile:
- completion?(.personaliseProfile)
+ completion?(.personalizeProfile)
case .takeMeHome:
completion?(.takeMeHome)
}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift
index 5b46e5c97..f82b46245 100644
--- a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift
@@ -31,10 +31,26 @@ class OnboardingCongratulationsUITests: MockScreenTest {
func verifyOnboardingCongratulationsScreen() throws {
guard let screenState = screenState as? MockOnboardingCongratulationsScreenState else { fatalError("no screen") }
switch screenState {
- case .congratulations:
- // There isn't anything to test here
- break
+ case .regular:
+ verifyButtons()
+ case .personalizationDisabled:
+ verifyButtonsWhenPersonalizationIsDisabled()
}
}
-
+
+ func verifyButtons() {
+ let personalizeButton = app.buttons["personalizeButton"]
+ XCTAssertTrue(personalizeButton.exists, "The personalization button should be shown.")
+
+ let homeButton = app.buttons["homeButton"]
+ XCTAssertTrue(homeButton.exists, "The home button should always be shown.")
+ }
+
+ func verifyButtonsWhenPersonalizationIsDisabled() {
+ let personalizeButton = app.buttons["personalizeButton"]
+ XCTAssertFalse(personalizeButton.exists, "The personalization button should be hidden.")
+
+ let homeButton = app.buttons["homeButton"]
+ XCTAssertTrue(homeButton.exists, "The home button should always be shown.")
+ }
}
diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift
index 892f904fc..244eba3a1 100644
--- a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift
@@ -45,7 +45,7 @@ struct OnboardingCongratulationsScreen: View {
Spacer()
- buttons
+ footer
.padding(.horizontal, horizontalPadding)
.padding(.bottom, 24)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
@@ -62,8 +62,10 @@ struct OnboardingCongratulationsScreen: View {
/// The main content of the view to be shown in a scroll view.
var mainContent: some View {
- VStack(spacing: 62) {
+ VStack(spacing: 42) {
Image(Asset.Images.onboardingCongratulationsIcon.name)
+ .resizable()
+ .frame(width: 90, height: 90)
.accessibilityHidden(true)
VStack(spacing: 8) {
@@ -79,23 +81,45 @@ struct OnboardingCongratulationsScreen: View {
}
}
- /// The action buttons shown at the bottom of the view.
- var buttons: some View {
+ @ViewBuilder
+ var footer: some View {
+ if viewModel.viewState.personalizationDisabled {
+ homeButton
+ } else {
+ actionButtons
+ }
+ }
+
+ /// The default action buttons shown at the bottom of the view.
+ var actionButtons: some View {
VStack(spacing: 12) {
Button { viewModel.send(viewAction: .personaliseProfile) } label: {
- Text(VectorL10n.onboardingCongratulationsPersonaliseButton)
- .font(theme.fonts.bodySB)
+ Text(VectorL10n.onboardingCongratulationsPersonalizeButton)
+ .font(theme.fonts.body)
.foregroundColor(theme.colors.accent)
}
.buttonStyle(PrimaryActionButtonStyle(customColor: .white))
+ .accessibilityIdentifier("personalizeButton")
Button { viewModel.send(viewAction: .takeMeHome) } label: {
Text(VectorL10n.onboardingCongratulationsHomeButton)
.font(theme.fonts.body)
.padding(.vertical, 12)
}
+ .accessibilityIdentifier("homeButton")
}
}
+
+ /// The single "Take me home" button shown when personlization isn't supported.
+ var homeButton: some View {
+ Button { viewModel.send(viewAction: .takeMeHome) } label: {
+ Text(VectorL10n.onboardingCongratulationsHomeButton)
+ .font(theme.fonts.body)
+ .foregroundColor(theme.colors.accent)
+ }
+ .buttonStyle(PrimaryActionButtonStyle(customColor: .white))
+ .accessibilityIdentifier("homeButton")
+ }
}
// MARK: - Previews
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift
new file mode 100644
index 000000000..3e96952ae
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift
@@ -0,0 +1,107 @@
+//
+// 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 SwiftUI
+import CommonKit
+
+struct OnboardingDisplayNameCoordinatorParameters {
+ let userSession: UserSession
+}
+
+@available(iOS 14.0, *)
+final class OnboardingDisplayNameCoordinator: Coordinator, Presentable {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: OnboardingDisplayNameCoordinatorParameters
+ private let onboardingDisplayNameHostingController: VectorHostingController
+ private var onboardingDisplayNameViewModel: OnboardingDisplayNameViewModelProtocol
+
+ private var indicatorPresenter: UserIndicatorTypePresenterProtocol
+ private var waitingIndicator: UserIndicator?
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+ var completion: ((UserSession) -> Void)?
+
+ // MARK: - Setup
+
+ init(parameters: OnboardingDisplayNameCoordinatorParameters) {
+ self.parameters = parameters
+
+ // Don't pre-fill the display name from the MXID to encourage the user to enter something
+ let viewModel = OnboardingDisplayNameViewModel()
+
+ let view = OnboardingDisplayNameScreen(viewModel: viewModel.context)
+ onboardingDisplayNameViewModel = viewModel
+ onboardingDisplayNameHostingController = VectorHostingController(rootView: view)
+ onboardingDisplayNameHostingController.vc_removeBackTitle()
+ onboardingDisplayNameHostingController.enableNavigationBarScrollEdgeAppearance = true
+
+ indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: onboardingDisplayNameHostingController)
+ }
+
+ // MARK: - Public
+ func start() {
+ MXLog.debug("[OnboardingDisplayNameCoordinator] did start.")
+ onboardingDisplayNameViewModel.completion = { [weak self] result in
+ guard let self = self else { return }
+ MXLog.debug("[OnboardingDisplayNameCoordinator] OnboardingDisplayNameViewModel did complete.")
+
+ switch result {
+ case .save(let displayName):
+ self.setDisplayName(displayName)
+ case .skip:
+ self.completion?(self.parameters.userSession)
+ }
+ }
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.onboardingDisplayNameHostingController
+ }
+
+ // MARK: - Private
+
+ /// Show a blocking activity indicator whilst saving.
+ private func startWaiting() {
+ waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.saving, isInteractionBlocking: true))
+ }
+
+ /// Hide the currently displayed activity indicator.
+ private func stopWaiting() {
+ waitingIndicator = nil
+ }
+
+ /// Set the supplied string as user's display name, completing the screen's display if successful.
+ private func setDisplayName(_ displayName: String) {
+ startWaiting()
+
+ parameters.userSession.account.setUserDisplayName(displayName) { [weak self] in
+ guard let self = self else { return }
+ self.stopWaiting()
+ self.completion?(self.parameters.userSession)
+ } failure: { [weak self] error in
+ guard let self = self else { return }
+ self.stopWaiting()
+ self.onboardingDisplayNameViewModel.processError(error as NSError?)
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/MockOnboardingDisplayNameScreenState.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/MockOnboardingDisplayNameScreenState.swift
new file mode 100644
index 000000000..797908a9e
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/MockOnboardingDisplayNameScreenState.swift
@@ -0,0 +1,63 @@
+//
+// 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 SwiftUI
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+@available(iOS 14.0, *)
+enum MockOnboardingDisplayNameScreenState: MockScreenState, CaseIterable {
+ // A case for each state you want to represent
+ // with specific, minimal associated data that will allow you
+ // mock that screen.
+ case emptyTextField
+ case filledTextField(displayName: String)
+ case longDisplayName(displayName: String)
+
+ /// The associated screen
+ var screenType: Any.Type {
+ OnboardingDisplayNameScreen.self
+ }
+
+ /// A list of screen state definitions
+ static var allCases: [MockOnboardingDisplayNameScreenState] {
+ [
+ MockOnboardingDisplayNameScreenState.emptyTextField,
+ MockOnboardingDisplayNameScreenState.filledTextField(displayName: "Test User"),
+ MockOnboardingDisplayNameScreenState.longDisplayName(displayName: """
+ Bacon ipsum dolor amet filet mignon chicken kevin andouille. Doner shoulder beef, brisket bresaola turkey jowl venison. Ham hock cow turducken, chislic venison doner short loin strip steak tri-tip jowl. Sirloin pork belly hamburger ribeye. Tail capicola alcatra short ribs turkey doner.
+ """)
+ ]
+ }
+
+ /// Generate the view struct for the screen state.
+ var screenView: ([Any], AnyView) {
+ let viewModel: OnboardingDisplayNameViewModel
+ switch self {
+ case .emptyTextField:
+ viewModel = OnboardingDisplayNameViewModel()
+ case .filledTextField(let displayName), .longDisplayName(displayName: let displayName):
+ viewModel = OnboardingDisplayNameViewModel(displayName: displayName)
+ }
+
+ // can simulate service and viewModel actions here if needs be.
+
+ return (
+ [self, viewModel], AnyView(OnboardingDisplayNameScreen(viewModel: viewModel.context))
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameModels.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameModels.swift
new file mode 100644
index 000000000..22857f69c
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameModels.swift
@@ -0,0 +1,55 @@
+//
+// 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
+
+// MARK: View model
+
+enum OnboardingDisplayNameViewModelResult {
+ /// The user would like to save the entered display name.
+ case save(String)
+ /// Move on to the next screen in the flow without setting a display name.
+ case skip
+}
+
+// MARK: View
+
+struct OnboardingDisplayNameViewState: BindableState {
+ var bindings: OnboardingDisplayNameBindings
+ /// Any error that occurred during display name validation otherwise `nil`.
+ var validationErrorMessage: String?
+
+ /// The string to be displayed in the text field's footer.
+ var textFieldFooterMessage: String {
+ validationErrorMessage ?? VectorL10n.onboardingDisplayNameHint
+ }
+}
+
+struct OnboardingDisplayNameBindings {
+ /// The display name string entered by the user.
+ var displayName: String
+ /// The currently displayed alert's info value otherwise `nil`.
+ var alertInfo: AlertInfo?
+}
+
+enum OnboardingDisplayNameViewAction {
+ /// The display name needs validation.
+ case validateDisplayName
+ /// The user would like to save the entered display name.
+ case save
+ /// Move on to the next screen in the flow without setting a display name.
+ case skip
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift
new file mode 100644
index 000000000..e72b0b7be
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift
@@ -0,0 +1,70 @@
+//
+// 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 SwiftUI
+import Combine
+
+@available(iOS 14, *)
+typealias OnboardingDisplayNameViewModelType = StateStoreViewModel
+@available(iOS 14, *)
+class OnboardingDisplayNameViewModel: OnboardingDisplayNameViewModelType, OnboardingDisplayNameViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ // MARK: Public
+
+ var completion: ((OnboardingDisplayNameViewModelResult) -> Void)?
+
+ // MARK: - Setup
+
+ init(displayName: String = "") {
+ super.init(initialViewState: OnboardingDisplayNameViewState(bindings: OnboardingDisplayNameBindings(displayName: displayName)))
+ validateDisplayName()
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: OnboardingDisplayNameViewAction) {
+ switch viewAction {
+ case .validateDisplayName:
+ validateDisplayName()
+ case .save:
+ completion?(.save(state.bindings.displayName))
+ case .skip:
+ completion?(.skip)
+ }
+ }
+
+ func processError(_ error: NSError?) {
+ state.bindings.alertInfo = AlertInfo(error: error)
+ }
+
+ // MARK: - Private
+
+ /// Checks for a display name that exceeds 256 characters and updates the footer error if needed.
+ private func validateDisplayName() {
+ if state.bindings.displayName.count > 256 {
+ guard state.validationErrorMessage == nil else { return }
+ state.validationErrorMessage = VectorL10n.onboardingDisplayNameMaxLength
+ } else if state.validationErrorMessage != nil {
+ state.validationErrorMessage = nil
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift
new file mode 100644
index 000000000..d03c8416e
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift
@@ -0,0 +1,28 @@
+//
+// 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 OnboardingDisplayNameViewModelProtocol {
+
+ var completion: ((OnboardingDisplayNameViewModelResult) -> Void)? { get set }
+ @available(iOS 14, *)
+ var context: OnboardingDisplayNameViewModelType.Context { get }
+
+ /// Update the view model to show that an error has occurred.
+ /// - Parameter error: The error to be displayed or `nil` to display a generic alert.
+ func processError(_ error: NSError?)
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift
new file mode 100644
index 000000000..30aa2ba50
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.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 XCTest
+import RiotSwiftUI
+
+@available(iOS 14.0, *)
+class OnboardingDisplayNameUITests: MockScreenTest {
+
+ override class var screenType: MockScreenState.Type {
+ return MockOnboardingDisplayNameScreenState.self
+ }
+
+ override class func createTest() -> MockScreenTest {
+ return OnboardingDisplayNameUITests(selector: #selector(verifyOnboardingDisplayNameScreen))
+ }
+
+ func verifyOnboardingDisplayNameScreen() throws {
+ guard let screenState = screenState as? MockOnboardingDisplayNameScreenState else { fatalError("no screen") }
+ switch screenState {
+ case .emptyTextField:
+ verifyEmptyTextField()
+ case .filledTextField(let displayName):
+ verifyDisplayName(displayName: displayName)
+ case .longDisplayName(displayName: let displayName):
+ verifyLongDisplayName(displayName: displayName)
+ }
+ }
+
+ func verifyEmptyTextField() {
+ let textField = app.textFields.element
+ XCTAssertTrue(textField.exists, "The textfield should always be shown.")
+ XCTAssertEqual(textField.value as? String, VectorL10n.onboardingDisplayNamePlaceholder, "When the textfield is empty, the value should match the placeholder.")
+ XCTAssertEqual(textField.placeholderValue, VectorL10n.onboardingDisplayNamePlaceholder, "The textfield's placeholder should be set.")
+
+ let footer = app.staticTexts["textFieldFooter"]
+ XCTAssertTrue(footer.exists, "The textfield's footer should always be shown.")
+ XCTAssertEqual(footer.label, VectorL10n.onboardingDisplayNameHint, "The footer should display a hint when no text is set.")
+
+ let saveButton = app.buttons["saveButton"]
+ XCTAssertTrue(saveButton.exists, "There should be a save button.")
+ XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.")
+ }
+
+ func verifyDisplayName(displayName: String) {
+ let textField = app.textFields.element
+ XCTAssertTrue(textField.exists, "The textfield should always be shown.")
+ XCTAssertEqual(textField.value as? String, displayName, "When a name has been set, it should show in the textfield.")
+ XCTAssertEqual(textField.placeholderValue, VectorL10n.onboardingDisplayNamePlaceholder, "The textfield's placeholder should be set.")
+
+ let saveButton = app.buttons["saveButton"]
+ XCTAssertTrue(saveButton.exists, "There should be a save button.")
+ XCTAssertTrue(saveButton.isEnabled, "The save button should be enabled.")
+
+ let footer = app.staticTexts["textFieldFooter"]
+ XCTAssertTrue(footer.exists, "The textfield's footer should always be shown.")
+ XCTAssertEqual(footer.label, VectorL10n.onboardingDisplayNameHint, "The footer should display a hint when an acceptable name is entered.")
+ }
+
+ func verifyLongDisplayName(displayName: String) {
+ let textField = app.textFields.element
+ XCTAssertTrue(textField.exists, "The textfield should always be shown.")
+ XCTAssertEqual(textField.value as? String, displayName, "When a name has been set, it should show in the textfield.")
+ XCTAssertEqual(textField.placeholderValue, VectorL10n.onboardingDisplayNamePlaceholder, "The textfield's placeholder should be set.")
+
+ let footer = app.staticTexts["textFieldFooter"]
+ XCTAssertTrue(footer.exists, "The textfield's footer should always be shown.")
+ XCTAssertEqual(footer.label, VectorL10n.onboardingDisplayNameMaxLength, "The footer should display an error when the display name is too long.")
+
+ let saveButton = app.buttons["saveButton"]
+ XCTAssertTrue(saveButton.exists, "There should be a save button.")
+ XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.")
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift
new file mode 100644
index 000000000..7f49a2aa2
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift
@@ -0,0 +1,64 @@
+//
+// 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 XCTest
+import Combine
+
+@testable import RiotSwiftUI
+
+@available(iOS 14.0, *)
+class OnboardingDisplayNameViewModelTests: XCTestCase {
+ var viewModel: OnboardingDisplayNameViewModel!
+ var context: OnboardingDisplayNameViewModelType.Context!
+
+ override func setUpWithError() throws {
+ viewModel = nil
+ context = nil
+ }
+
+ func setUp(with displayName: String) {
+ viewModel = OnboardingDisplayNameViewModel(displayName: displayName)
+ context = viewModel.context
+ }
+
+ func testValidDisplayName() {
+ // Given a short display name
+ let displayName = "Alice"
+ setUp(with: displayName)
+
+ // When validating the display name
+ viewModel.process(viewAction: .validateDisplayName)
+
+ // Then no error message should be set
+ XCTAssertEqual(context.viewState.bindings.displayName, displayName, "The display name should match the value used at init.")
+ XCTAssertNil(context.viewState.validationErrorMessage, "There should not be an error message in the view state.")
+ }
+
+ func testInvalidDisplayName() {
+ // Given a short display name
+ let displayName = """
+ Bacon ipsum dolor amet filet mignon chicken kevin andouille. Doner shoulder beef, brisket bresaola turkey jowl venison. Ham hock cow turducken, chislic venison doner short loin strip steak tri-tip jowl. Sirloin pork belly hamburger ribeye. Tail capicola alcatra short ribs turkey doner.
+ """
+ setUp(with: displayName)
+
+ // When validating the display name
+ viewModel.process(viewAction: .validateDisplayName)
+
+ // Then no error message should be set
+ XCTAssertEqual(context.viewState.bindings.displayName, displayName, "The display name should match the value used at init.")
+ XCTAssertNotNil(context.viewState.validationErrorMessage, "There should be an error message in the view state.")
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift
new file mode 100644
index 000000000..a678e1aa3
--- /dev/null
+++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift
@@ -0,0 +1,139 @@
+//
+// 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 SwiftUI
+
+@available(iOS 14.0, *)
+struct OnboardingDisplayNameScreen: View {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme: ThemeSwiftUI
+
+ @State private var isEditingTextField = false
+
+ private var textFieldFooterColor: Color {
+ viewModel.viewState.validationErrorMessage == nil ? theme.colors.tertiaryContent : theme.colors.alert
+ }
+
+ // MARK: Public
+
+ @ObservedObject var viewModel: OnboardingDisplayNameViewModel.Context
+
+ // MARK: - Views
+
+ var body: some View {
+ ScrollView {
+ VStack(spacing: 0) {
+ header
+ .padding(.bottom, 32)
+
+ textField
+ .padding(.horizontal, 2)
+ .padding(.bottom, 20)
+
+ buttons
+ }
+ .padding(.horizontal)
+ .padding(.top, 8)
+ .frame(maxWidth: OnboardingConstants.maxContentWidth)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .accentColor(theme.colors.accent)
+ .background(theme.colors.background.ignoresSafeArea())
+ .alert(item: $viewModel.alertInfo) { $0.alert }
+ .onChange(of: viewModel.displayName) { _ in
+ viewModel.send(viewAction: .validateDisplayName)
+ }
+ }
+
+ /// The icon, title and message views.
+ var header: some View {
+ VStack(spacing: 8) {
+ Image(Asset.Images.onboardingCongratulationsIcon.name)
+ .resizable()
+ .renderingMode(.template)
+ .foregroundColor(theme.colors.accent)
+ .frame(width: 90, height: 90)
+ .background(Circle().foregroundColor(.white).padding(2))
+ .padding(.bottom, 8)
+ .accessibilityHidden(true)
+
+ Text(VectorL10n.onboardingDisplayNameTitle)
+ .font(theme.fonts.title2B)
+ .multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.primaryContent)
+
+ Text(VectorL10n.onboardingDisplayNameMessage)
+ .font(theme.fonts.subheadline)
+ .multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.secondaryContent)
+ }
+ }
+
+ /// The text field used to enter the displayname along with a hint.
+ var textField: some View {
+ VStack(spacing: 4) {
+ TextField(VectorL10n.onboardingDisplayNamePlaceholder, text: $viewModel.displayName) {
+ isEditingTextField = $0
+ }
+ .textFieldStyle(BorderedInputFieldStyle(theme: _theme,
+ isEditing: isEditingTextField,
+ isError: viewModel.viewState.validationErrorMessage != nil))
+
+ Text(viewModel.viewState.textFieldFooterMessage)
+ .font(theme.fonts.footnote)
+ .foregroundColor(textFieldFooterColor)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .accessibilityIdentifier("textFieldFooter")
+ }
+ }
+
+ /// The main action buttons in the form.
+ var buttons: some View {
+ VStack(spacing: 8) {
+ Button(VectorL10n.onboardingPersonalizationSave) {
+ viewModel.send(viewAction: .save)
+ }
+ .buttonStyle(PrimaryActionButtonStyle())
+ .disabled(viewModel.displayName.isEmpty || viewModel.viewState.validationErrorMessage != nil)
+ .accessibilityIdentifier("saveButton")
+
+ Button { viewModel.send(viewAction: .skip) } label: {
+ Text(VectorL10n.onboardingPersonalizationSkip)
+ .font(theme.fonts.body)
+ .padding(12)
+ }
+ }
+ }
+}
+
+// MARK: - Previews
+
+@available(iOS 15.0, *)
+struct OnboardingDisplayName_Previews: PreviewProvider {
+ static let stateRenderer = MockOnboardingDisplayNameScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup(addNavigation: true)
+ .navigationViewStyle(.stack)
+ .theme(.light).preferredColorScheme(.light)
+ stateRenderer.screenGroup(addNavigation: true)
+ .navigationViewStyle(.stack)
+ .theme(.dark).preferredColorScheme(.dark)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift
index d031d5826..fb0293acf 100644
--- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift
+++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift
@@ -20,13 +20,14 @@ protocol OnboardingSplashScreenCoordinatorProtocol: Coordinator, Presentable {
var completion: ((OnboardingSplashScreenViewModelResult) -> Void)? { get set }
}
+@available(iOS 14.0, *)
final class OnboardingSplashScreenCoordinator: OnboardingSplashScreenCoordinatorProtocol {
// MARK: - Properties
// MARK: Private
- private let onboardingSplashScreenHostingController: UIViewController
+ private let onboardingSplashScreenHostingController: VectorHostingController
private var onboardingSplashScreenViewModel: OnboardingSplashScreenViewModelProtocol
// MARK: Public
@@ -37,14 +38,12 @@ final class OnboardingSplashScreenCoordinator: OnboardingSplashScreenCoordinator
// MARK: - Setup
- @available(iOS 14.0, *)
init() {
let viewModel = OnboardingSplashScreenViewModel()
let view = OnboardingSplashScreen(viewModel: viewModel.context)
onboardingSplashScreenViewModel = viewModel
- let hostingController = VectorHostingController(rootView: view)
- hostingController.vc_removeBackTitle()
- onboardingSplashScreenHostingController = hostingController
+ onboardingSplashScreenHostingController = VectorHostingController(rootView: view)
+ onboardingSplashScreenHostingController.vc_removeBackTitle()
}
// MARK: - Public
diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift
index e6473eeb4..e900c3845 100644
--- a/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift
+++ b/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift
@@ -16,13 +16,14 @@
import SwiftUI
+@available(iOS 14.0, *)
final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
- private let onboardingUseCaseHostingController: UIViewController
+ private let onboardingUseCaseHostingController: VectorHostingController
private var onboardingUseCaseViewModel: OnboardingUseCaseViewModelProtocol
// MARK: Public
@@ -33,16 +34,14 @@ final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable {
// MARK: - Setup
- @available(iOS 14.0, *)
init() {
let viewModel = OnboardingUseCaseViewModel()
let view = OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
onboardingUseCaseViewModel = viewModel
- let hostingController = VectorHostingController(rootView: view)
- hostingController.vc_removeBackTitle()
- hostingController.enableNavigationBarScrollEdgeAppearance = true
- onboardingUseCaseHostingController = hostingController
+ onboardingUseCaseHostingController = VectorHostingController(rootView: view)
+ onboardingUseCaseHostingController.vc_removeBackTitle()
+ onboardingUseCaseHostingController.enableNavigationBarScrollEdgeAppearance = true
}
// MARK: - Public
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift
index 9247da882..2c64a3258 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingOptionButton.swift
@@ -54,7 +54,7 @@ struct LocationSharingOptionButton_Previews: PreviewProvider {
LocationSharingOptionButton(text: "Share live location") {
} content: {
- LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.locationLiveIcon.image)
+ LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.liveLocationIcon.image)
}
}
}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift
index f3f7e417e..43f77cc92 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift
@@ -99,7 +99,7 @@ struct LocationSharingView: View {
LocationSharingOptionButton(text: VectorL10n.locationSharingLiveShareTitle) {
// TODO: - Start live location sharing
} content: {
- LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.locationLiveIcon.image)
+ LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.liveLocationIcon.image)
}
.disabled(!context.viewState.shareButtonEnabled)
}
diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift
index 51a3aa7f7..da4e3fbad 100644
--- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift
@@ -46,8 +46,8 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
super.init(initialViewState: UserSuggestionViewState(items: items))
- userSuggestionService.items.sink { items in
- self.state.items = items.map({ item in
+ userSuggestionService.items.sink { [weak self] items in
+ self?.state.items = items.map({ item in
UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName)
})
}.store(in: &cancellables)
diff --git a/SiriIntents/IntentHandler.m b/SiriIntents/IntentHandler.m
index 8220d9fed..94d120a4f 100644
--- a/SiriIntents/IntentHandler.m
+++ b/SiriIntents/IntentHandler.m
@@ -27,6 +27,12 @@
// Build Settings
@property (nonatomic) id configuration;
+/**
+ The room that is currently being used to send a message. This is to ensure a
+ strong ref is maintained on the `MXRoom` until sending has completed.
+ */
+@property (nonatomic) MXRoom *selectedRoom;
+
@end
@implementation IntentHandler
@@ -242,17 +248,22 @@
[session setStore:fileStore success:^{
MXStrongifyAndReturnIfNil(session);
- MXRoom *room = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session];
+ self.selectedRoom = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session];
// Do not warn for unknown devices. We have cross-signing now
session.crypto.warnOnUnknowDevices = NO;
- [room sendTextMessage:intent.content
- threadId:nil
- success:^(NSString *eventId) {
+ MXWeakify(self);
+ [self.selectedRoom sendTextMessage:intent.content
+ threadId:nil
+ success:^(NSString *eventId) {
completeWithCode(INSendMessageIntentResponseCodeSuccess);
+ MXStrongifyAndReturnIfNil(self);
+ self.selectedRoom = nil;
} failure:^(NSError *error) {
completeWithCode(INSendMessageIntentResponseCodeFailure);
+ MXStrongifyAndReturnIfNil(self);
+ self.selectedRoom = nil;
}];
} failure:^(NSError *error) {
diff --git a/changelog.d/5058.bugfix b/changelog.d/5058.bugfix
new file mode 100644
index 000000000..c1bba1101
--- /dev/null
+++ b/changelog.d/5058.bugfix
@@ -0,0 +1 @@
+UserSuggestionViewModel: Fix retain cycle
diff --git a/changelog.d/5500.change b/changelog.d/5500.change
new file mode 100644
index 000000000..bcfe8ba8a
--- /dev/null
+++ b/changelog.d/5500.change
@@ -0,0 +1 @@
+Change behaviour of avatar/self in left menu to match common paradigm and take user to their own profile/settings
\ No newline at end of file
diff --git a/changelog.d/5547.bugfix b/changelog.d/5547.bugfix
new file mode 100644
index 000000000..570df2392
--- /dev/null
+++ b/changelog.d/5547.bugfix
@@ -0,0 +1 @@
+Home: Fix crash when pressing tabs
diff --git a/changelog.d/5652.wip b/changelog.d/5652.wip
new file mode 100644
index 000000000..0d173fbd5
--- /dev/null
+++ b/changelog.d/5652.wip
@@ -0,0 +1 @@
+Onboarding: Add screens for setting a display name and avatar when signing up for the first time.
\ No newline at end of file
diff --git a/changelog.d/5805.bugfix b/changelog.d/5805.bugfix
new file mode 100644
index 000000000..831afcca9
--- /dev/null
+++ b/changelog.d/5805.bugfix
@@ -0,0 +1 @@
+Share Extension: Stop logging crashes due to intentional exception that frees up memory and handle changes to MXRoom in the SDK.
diff --git a/changelog.d/5846.bugfix b/changelog.d/5846.bugfix
new file mode 100644
index 000000000..d37861d55
--- /dev/null
+++ b/changelog.d/5846.bugfix
@@ -0,0 +1 @@
+Authentication: Fix a crash that occurred when using the app with an account that had a soft logout.
\ No newline at end of file
diff --git a/changelog.d/5853.change b/changelog.d/5853.change
new file mode 100644
index 000000000..c35b4255a
--- /dev/null
+++ b/changelog.d/5853.change
@@ -0,0 +1 @@
+RoomViewController: Remove thread list bar button item badge count.
diff --git a/changelog.d/5857.wip b/changelog.d/5857.wip
new file mode 100644
index 000000000..80369f90c
--- /dev/null
+++ b/changelog.d/5857.wip
@@ -0,0 +1 @@
+Location sharing: Handle live location banner view in room screen.
\ No newline at end of file
diff --git a/changelog.d/5873.bugfix b/changelog.d/5873.bugfix
new file mode 100644
index 000000000..cf3c09280
--- /dev/null
+++ b/changelog.d/5873.bugfix
@@ -0,0 +1 @@
+MXAccount: Do not clear cache if there are no stored filters
diff --git a/changelog.d/pr-5876.bugfix b/changelog.d/pr-5876.bugfix
new file mode 100644
index 000000000..aca03fdfa
--- /dev/null
+++ b/changelog.d/pr-5876.bugfix
@@ -0,0 +1 @@
+Fix user suggestions not showing up when re-entering a room.
\ No newline at end of file