diff --git a/.gitignore b/.gitignore
index 695d4cd61..2530cc0a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,8 +30,14 @@ vendor/
Pods/
## Ignore project files as we generate them with xcodegen (https://github.com/yonaskolb/XcodeGen)
+# Plus ridiculous workaround to unignore the Package.resolved file for SwiftPM.
*.xcodeproj
-*.xcworkspace
+*.xcworkspace/*
+!Riot.xcworkspace/xcshareddata
+Riot.xcworkspace/xcshareddata/*
+!Riot.xcworkspace/xcshareddata/swiftpm/
+Riot.xcworkspace/xcshareddata/swiftpm/*
+!Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
# Fastlane
fastlane/report.xml
diff --git a/CHANGES.md b/CHANGES.md
index 4457e8f2f..5065fd74e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,50 @@
+## Changes in 1.8.21 (2022-07-12)
+
+✨ Features
+
+- Analytics: Track non-fatal issues if consent provided ([#6308](https://github.com/vector-im/element-ios/pull/6308))
+- Notifications: Add a setting for in-app notifications and use the value with existing functionality in PushNotificationService. ([#1108](https://github.com/vector-im/element-ios/issues/1108))
+- Server Offline Activity Indicator ([#5607](https://github.com/vector-im/element-ios/issues/5607))
+
+🙌 Improvements
+
+- Add formatter build reply HTML unit tests ([#6380](https://github.com/vector-im/element-ios/pull/6380))
+- Upgrade MatrixSDK version ([v0.23.11](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.11)).
+- Update Files component ([#5372](https://github.com/vector-im/element-ios/issues/5372))
+- Location sharing: Update map credits display and behavior. ([#6108](https://github.com/vector-im/element-ios/issues/6108))
+- Location sharing: Add view to promote live location sharing labs flag on the sharing screen. ([#6238](https://github.com/vector-im/element-ios/issues/6238))
+- Remove legacy Riot-Defaults property list ([#6273](https://github.com/vector-im/element-ios/issues/6273))
+- DesignKit: Replace the local DesignKit target with the shared Swift package from ElementX. ([#6276](https://github.com/vector-im/element-ios/issues/6276))
+- Enhance the VectorHostingController to be presented as a bottom sheet ([#6376](https://github.com/vector-im/element-ios/issues/6376))
+- Location sharing: Live location sharing UI polishing. ([#6382](https://github.com/vector-im/element-ios/issues/6382))
+
+🐛 Bugfixes
+
+- VectorHostingController: Fix infinite loop due to the safe area insets fix. ([#6381](https://github.com/vector-im/element-ios/pull/6381))
+- Fix layout issues in timeline poll cells (PSB-125) ([#5326](https://github.com/vector-im/element-ios/issues/5326))
+- Fixed Invite user UI is always hidden by the keyboard ([#5341](https://github.com/vector-im/element-ios/issues/5341))
+- Cross-Signing: Use ZXing library to generate QR codes ([#6358](https://github.com/vector-im/element-ios/issues/6358))
+- Location sharing: Fix live location sharing lab flag activation, no more app relaunch needed. ([#6361](https://github.com/vector-im/element-ios/issues/6361))
+- Display fallback when replied event content is partially missing ([#6371](https://github.com/vector-im/element-ios/issues/6371))
+- Fix a few failing UI tests. ([#6386](https://github.com/vector-im/element-ios/issues/6386))
+- Rename riot-keys.txt to element-keys.txt. ([#6391](https://github.com/vector-im/element-ios/issues/6391))
+- Fix inoperant room links with alias/identifiers ([#6395](https://github.com/vector-im/element-ios/issues/6395))
+- Fix slash commands from room composer ([#6398](https://github.com/vector-im/element-ios/issues/6398))
+
+⚠️ API Changes
+
+- Replace DesignKit framework with [DesignKit package](https://github.com/vector-im/element-x-ios/tree/develop/DesignKit/Sources). Colours are now generated in the [DesignTokens repo](https://github.com/vector-im/element-design-tokens) to be shared across all of our apps. ([#6275](https://github.com/vector-im/element-ios/pull/6275))
+
+🧱 Build
+
+- Update Podfile.lock ([#6387](https://github.com/vector-im/element-ios/pull/6387))
+- Split `IntentHandler` into smaller, dedicated entities ([#6203](https://github.com/vector-im/element-ios/issues/6203))
+
+Others
+
+- Revert some font changes made when merging #6392. ([#6392](https://github.com/vector-im/element-ios/issues/6392))
+
+
## Changes in 1.8.20 (2022-06-28)
✨ Features
diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig
index c6678098a..72a69d031 100644
--- a/Config/AppVersion.xcconfig
+++ b/Config/AppVersion.xcconfig
@@ -15,5 +15,5 @@
//
// Version
-MARKETING_VERSION = 1.8.20
-CURRENT_PROJECT_VERSION = 1.8.20
+MARKETING_VERSION = 1.8.21
+CURRENT_PROJECT_VERSION = 1.8.21
diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift
index 9d307cbef..31f0c737a 100644
--- a/Config/BuildSettings.swift
+++ b/Config/BuildSettings.swift
@@ -405,4 +405,17 @@ final class BuildSettings: NSObject {
static let tileServerMapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
static let locationSharingEnabled = true
+
+ // MARK: - MXKAppSettings
+ static let enableBotCreation: Bool = false
+ static let maxAllowedMediaCacheSize: Int = 1073741824
+ static let presenceColorForOfflineUser: Int = 15020851
+ static let presenceColorForOnlineUser: Int = 3401011
+ static let presenceColorForUnavailableUser: Int = 15066368
+ static let showAllEventsInRoomHistory: Bool = false
+ static let showLeftMembersInRoomMemberList: Bool = false
+ static let showRedactionsInRoomHistory: Bool = true
+ static let showUnsupportedEventsInRoomHistory: Bool = false
+ static let sortRoomMembersUsingLastSeenTime: Bool = true
+ static let syncLocalContacts: Bool = false
}
diff --git a/DesignKit/Extensions/UIFont.swift b/DesignKit/Extensions/UIFont.swift
deleted file mode 100644
index 7804c8066..000000000
--- a/DesignKit/Extensions/UIFont.swift
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// 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
-
-public extension UIFont {
-
- // MARK: - Convenient methods
-
- /// Update current font with a SymbolicTraits
- func vc_withTraits(_ traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
- guard let descriptor = fontDescriptor.withSymbolicTraits(traits) else {
- return self
- }
- return UIFont(descriptor: descriptor, size: 0) // Size 0 means keep the size as it is
- }
-
- /// Update current font with a given Weight
- func vc_withWeight(weight: Weight) -> UIFont {
- // Add the font weight to the descriptor
- let weightedFontDescriptor = fontDescriptor.addingAttributes([
- UIFontDescriptor.AttributeName.traits: [
- UIFontDescriptor.TraitKey.weight: weight
- ]
- ])
- return UIFont(descriptor: weightedFontDescriptor, size: 0)
- }
-
- // MARK: - Shortcuts
-
- var vc_bold: UIFont {
- return self.vc_withTraits(.traitBold)
- }
-
- var vc_semiBold: UIFont {
- return self.vc_withWeight(weight: .semibold)
- }
-
- var vc_italic: UIFont {
- return self.vc_withTraits(.traitItalic)
- }
-}
diff --git a/DesignKit/Info.plist b/DesignKit/Info.plist
deleted file mode 100644
index c0701c6d7..000000000
--- a/DesignKit/Info.plist
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- $(MARKETING_VERSION)
- CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
-
-
diff --git a/DesignKit/Source/ColorValues.swift b/DesignKit/Source/ColorValues.swift
deleted file mode 100644
index 338d1cfe8..000000000
--- a/DesignKit/Source/ColorValues.swift
+++ /dev/null
@@ -1,52 +0,0 @@
-//
-// Copyright 2021 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import UIKit
-
-/**
- Struct for holding colour values for a particular theme.
- */
-public struct ColorValues: Colors {
-
- public let accent: UIColor
-
- public let alert: UIColor
-
- public let primaryContent: UIColor
-
- public let secondaryContent: UIColor
-
- public let tertiaryContent: UIColor
-
- public let quarterlyContent: UIColor
-
- public let quinaryContent: UIColor
-
- public let separator: UIColor
-
- public let system: UIColor
-
- public let tile: UIColor
-
- public let navigation: UIColor
-
- public let background: UIColor
-
- public let ems: UIColor
-
- public let namesAndAvatars: [UIColor]
-}
diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift
deleted file mode 100644
index bf3e9abd3..000000000
--- a/DesignKit/Source/Colors.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-//
-// 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
-
-/// Colors at https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1255%3A1104
-public protocol Colors {
-
- associatedtype ColorType
-
- /// - Focused/Active states
- /// - CTAs
- var accent: ColorType { get }
-
- /// - Error messages
- /// - Content requiring user attention
- /// - Notification, alerts
- var alert: ColorType { get }
-
- /// - Text
- /// - Icons
- var primaryContent: ColorType { get }
-
- /// - Text
- /// - Icons
- var secondaryContent: ColorType { get }
-
- /// - Text
- /// - Icons
- var tertiaryContent: ColorType { get }
-
- /// - Text
- /// - Icons
- var quarterlyContent: ColorType { get }
-
- /// - separating lines and other UI components
- var quinaryContent: ColorType { get }
-
- /// - System-based areas and backgrounds
- var system: ColorType { get }
-
- /// Separating line
- var separator: ColorType { get }
-
- /// Cards, tiles
- var tile: ColorType { get }
-
- /// Top navigation background on iOS
- var navigation: ColorType { get }
-
- /// Background UI color
- var background: ColorType { get }
-
- /// Global color: The EMS brand's purple colour.
- var ems: ColorType { get }
-
- /// - Names in chat timeline
- /// - Avatars default states that include first name letter
- var namesAndAvatars: [ColorType] { get }
-}
diff --git a/DesignKit/Source/ColorsSwiftUI.swift b/DesignKit/Source/ColorsSwiftUI.swift
deleted file mode 100644
index ea3ca6779..000000000
--- a/DesignKit/Source/ColorsSwiftUI.swift
+++ /dev/null
@@ -1,69 +0,0 @@
-//
-// 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
-
-/**
- Struct for holding colors for use in SwiftUI.
- */
-public struct ColorSwiftUI: Colors {
-
- public let accent: Color
-
- public let alert: Color
-
- public let primaryContent: Color
-
- public let secondaryContent: Color
-
- public let tertiaryContent: Color
-
- public let quarterlyContent: Color
-
- public let quinaryContent: Color
-
- public let separator: Color
-
- public var system: Color
-
- public let tile: Color
-
- public let navigation: Color
-
- public let background: Color
-
- public var ems: Color
-
- public let namesAndAvatars: [Color]
-
- init(values: ColorValues) {
- accent = Color(values.accent)
- alert = Color(values.alert)
- primaryContent = Color(values.primaryContent)
- secondaryContent = Color(values.secondaryContent)
- tertiaryContent = Color(values.tertiaryContent)
- quarterlyContent = Color(values.quarterlyContent)
- quinaryContent = Color(values.quinaryContent)
- separator = Color(values.separator)
- system = Color(values.system)
- tile = Color(values.tile)
- navigation = Color(values.navigation)
- background = Color(values.background)
- ems = Color(values.ems)
- namesAndAvatars = values.namesAndAvatars.map({ Color($0) })
- }
-}
diff --git a/DesignKit/Source/ColorsUIkit.swift b/DesignKit/Source/ColorsUIkit.swift
deleted file mode 100644
index 3add385c3..000000000
--- a/DesignKit/Source/ColorsUIkit.swift
+++ /dev/null
@@ -1,67 +0,0 @@
-//
-// Copyright 2021 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import UIKit
-
-/**
- ObjC class for holding colors for use in UIKit.
- */
-@objcMembers public class ColorsUIKit: NSObject {
-
- public let accent: UIColor
-
- public let alert: UIColor
-
- public let primaryContent: UIColor
-
- public let secondaryContent: UIColor
-
- public let tertiaryContent: UIColor
-
- public let quarterlyContent: UIColor
-
- public let quinaryContent: UIColor
-
- public let separator: UIColor
-
- public let system: UIColor
-
- public let tile: UIColor
-
- public let navigation: UIColor
-
- public let background: UIColor
-
- public let namesAndAvatars: [UIColor]
-
- init(values: ColorValues) {
- accent = values.accent
- alert = values.alert
- primaryContent = values.primaryContent
- secondaryContent = values.secondaryContent
- tertiaryContent = values.tertiaryContent
- quarterlyContent = values.quarterlyContent
- quinaryContent = values.quinaryContent
- separator = values.separator
- system = values.system
- tile = values.tile
- navigation = values.navigation
- background = values.background
- namesAndAvatars = values.namesAndAvatars
- }
-}
-
diff --git a/DesignKit/Source/Fonts.swift b/DesignKit/Source/Fonts.swift
deleted file mode 100644
index 1203a2888..000000000
--- a/DesignKit/Source/Fonts.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-//
-// 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
-
-/// Describe fonts used in the application.
-/// Font names are based on Element typograhy https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1362%3A0 which is based on Apple font text styles (UIFont.TextStyle): https://developer.apple.com/documentation/uikit/uifonttextstyle
-/// Create a custom TextStyle enum (like DesignKit.Fonts.TextStyle) is also a possiblity
-public protocol Fonts {
-
- associatedtype FontType
-
- /// The font for large titles.
- var largeTitle: FontType { get }
-
- /// `largeTitle` with a Bold weight.
- var largeTitleB: FontType { get }
-
- /// The font for first-level hierarchical headings.
- var title1: FontType { get }
-
- /// `title1` with a Bold weight.
- var title1B: FontType { get }
-
- /// The font for second-level hierarchical headings.
- var title2: FontType { get }
-
- /// `title2` with a Bold weight.
- var title2B: FontType { get }
-
- /// The font for third-level hierarchical headings.
- var title3: FontType { get }
-
- /// `title3` with a Semi Bold weight.
- var title3SB: FontType { get }
-
- /// The font for headings.
- var headline: FontType { get }
-
- /// The font for subheadings.
- var subheadline: FontType { get }
-
- /// The font for body text.
- var body: FontType { get }
-
- /// `body` with a Semi Bold weight.
- var bodySB: FontType { get }
-
- /// The font for callouts.
- var callout: FontType { get }
-
- /// `callout` with a Semi Bold weight.
- var calloutSB: FontType { get }
-
- /// The font for footnotes.
- var footnote: FontType { get }
-
- /// `footnote` with a Semi Bold weight.
- var footnoteSB: FontType { get }
-
- /// The font for standard captions.
- var caption1: FontType { get }
-
- /// `caption1` with a Semi Bold weight.
- var caption1SB: FontType { get }
-
- /// The font for alternate captions.
- var caption2: FontType { get }
-
- /// `caption2` with a Semi Bold weight.
- var caption2SB: FontType { get }
-}
diff --git a/DesignKit/Source/FontsSwiftUI.swift b/DesignKit/Source/FontsSwiftUI.swift
deleted file mode 100644
index 83b4e820b..000000000
--- a/DesignKit/Source/FontsSwiftUI.swift
+++ /dev/null
@@ -1,91 +0,0 @@
-//
-// 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
-
-/**
- Struct for holding fonts for use in SwiftUI.
- */
-public struct FontSwiftUI: Fonts {
-
- public let uiFonts: FontsUIKit
-
- public var largeTitle: Font
-
- public var largeTitleB: Font
-
- public var title1: Font
-
- public var title1B: Font
-
- public var title2: Font
-
- public var title2B: Font
-
- public var title3: Font
-
- public var title3SB: Font
-
- public var headline: Font
-
- public var subheadline: Font
-
- public var body: Font
-
- public var bodySB: Font
-
- public var callout: Font
-
- public var calloutSB: Font
-
- public var footnote: Font
-
- public var footnoteSB: Font
-
- public var caption1: Font
-
- public var caption1SB: Font
-
- public var caption2: Font
-
- public var caption2SB: Font
-
- public init(values: ElementFonts) {
- self.uiFonts = FontsUIKit(values: values)
-
- self.largeTitle = values.largeTitle.font
- self.largeTitleB = values.largeTitleB.font
- self.title1 = values.title1.font
- self.title1B = values.title1B.font
- self.title2 = values.title2.font
- self.title2B = values.title2B.font
- self.title3 = values.title3.font
- self.title3SB = values.title3SB.font
- self.headline = values.headline.font
- self.subheadline = values.subheadline.font
- self.body = values.body.font
- self.bodySB = values.bodySB.font
- self.callout = values.callout.font
- self.calloutSB = values.calloutSB.font
- self.footnote = values.footnote.font
- self.footnoteSB = values.footnoteSB.font
- self.caption1 = values.caption1.font
- self.caption1SB = values.caption1SB.font
- self.caption2 = values.caption2.font
- self.caption2SB = values.caption2SB.font
- }
-}
diff --git a/DesignKit/Source/FontsUIkit.swift b/DesignKit/Source/FontsUIkit.swift
deleted file mode 100644
index ec65cdaa6..000000000
--- a/DesignKit/Source/FontsUIkit.swift
+++ /dev/null
@@ -1,87 +0,0 @@
-//
-// Copyright 2021 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import UIKit
-
-/**
- ObjC class for holding fonts for use in UIKit.
- */
-@objcMembers public class FontsUIKit: NSObject, Fonts {
-
- public var largeTitle: UIFont
-
- public var largeTitleB: UIFont
-
- public var title1: UIFont
-
- public var title1B: UIFont
-
- public var title2: UIFont
-
- public var title2B: UIFont
-
- public var title3: UIFont
-
- public var title3SB: UIFont
-
- public var headline: UIFont
-
- public var subheadline: UIFont
-
- public var body: UIFont
-
- public var bodySB: UIFont
-
- public var callout: UIFont
-
- public var calloutSB: UIFont
-
- public var footnote: UIFont
-
- public var footnoteSB: UIFont
-
- public var caption1: UIFont
-
- public var caption1SB: UIFont
-
- public var caption2: UIFont
-
- public var caption2SB: UIFont
-
- public init(values: ElementFonts) {
- self.largeTitle = values.largeTitle.uiFont
- self.largeTitleB = values.largeTitleB.uiFont
- self.title1 = values.title1.uiFont
- self.title1B = values.title1B.uiFont
- self.title2 = values.title2.uiFont
- self.title2B = values.title2B.uiFont
- self.title3 = values.title3.uiFont
- self.title3SB = values.title3SB.uiFont
- self.headline = values.headline.uiFont
- self.subheadline = values.subheadline.uiFont
- self.body = values.body.uiFont
- self.bodySB = values.bodySB.uiFont
- self.callout = values.callout.uiFont
- self.calloutSB = values.calloutSB.uiFont
- self.footnote = values.footnote.uiFont
- self.footnoteSB = values.footnoteSB.uiFont
- self.caption1 = values.caption1.uiFont
- self.caption1SB = values.caption1SB.uiFont
- self.caption2 = values.caption2.uiFont
- self.caption2SB = values.caption2SB.uiFont
- }
-}
diff --git a/DesignKit/Variants/Colors/Dark/DarkColors.swift b/DesignKit/Variants/Colors/Dark/DarkColors.swift
deleted file mode 100644
index 88bd12ff3..000000000
--- a/DesignKit/Variants/Colors/Dark/DarkColors.swift
+++ /dev/null
@@ -1,51 +0,0 @@
-//
-// Copyright 2021 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import UIKit
-import SwiftUI
-
-/// Dark theme colors.
-public class DarkColors {
- private static let values = ColorValues(
- accent: UIColor(rgb:0x0DBD8B),
- alert: UIColor(rgb:0xFF4B55),
- primaryContent: UIColor(rgb:0xFFFFFF),
- secondaryContent: UIColor(rgb:0xA9B2BC),
- tertiaryContent: UIColor(rgb:0x8E99A4),
- quarterlyContent: UIColor(rgb:0x6F7882),
- quinaryContent: UIColor(rgb:0x394049),
- separator: UIColor(rgb:0x21262C),
- system: UIColor(rgb:0x21262C),
- tile: UIColor(rgb:0x394049),
- navigation: UIColor(rgb:0x21262C),
- background: UIColor(rgb:0x15191E),
- ems: UIColor(rgb: 0x7E69FF),
- namesAndAvatars: [
- UIColor(rgb:0x368BD6),
- UIColor(rgb:0xAC3BA8),
- UIColor(rgb:0x03B381),
- UIColor(rgb:0xE64F7A),
- UIColor(rgb:0xFF812D),
- UIColor(rgb:0x2DC2C5),
- UIColor(rgb:0x5C56F5),
- UIColor(rgb:0x74D12C)
- ]
- )
-
- public static var uiKit = ColorsUIKit(values: values)
- public static var swiftUI = ColorSwiftUI(values: values)
-}
diff --git a/DesignKit/Variants/Colors/Light/LightColors.swift b/DesignKit/Variants/Colors/Light/LightColors.swift
deleted file mode 100644
index 93cb3eadb..000000000
--- a/DesignKit/Variants/Colors/Light/LightColors.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-//
-// Copyright 2021 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import UIKit
-import SwiftUI
-
-
-/// Light theme colors.
-public class LightColors {
- private static let values = ColorValues(
- accent: UIColor(rgb:0x0DBD8B),
- alert: UIColor(rgb:0xFF4B55),
- primaryContent: UIColor(rgb:0x17191C),
- secondaryContent: UIColor(rgb:0x737D8C),
- tertiaryContent: UIColor(rgb:0x8D97A5),
- quarterlyContent: UIColor(rgb:0xC1C6CD),
- quinaryContent: UIColor(rgb:0xE3E8F0),
- separator: UIColor(rgb:0xE3E8F0),
- system: UIColor(rgb:0xF4F6FA),
- tile: UIColor(rgb:0xF3F8FD),
- navigation: UIColor(rgb:0xF4F6FA),
- background: UIColor(rgb:0xFFFFFF),
- ems: UIColor(rgb: 0x7E69FF),
- namesAndAvatars: [
- UIColor(rgb:0x368BD6),
- UIColor(rgb:0xAC3BA8),
- UIColor(rgb:0x03B381),
- UIColor(rgb:0xE64F7A),
- UIColor(rgb:0xFF812D),
- UIColor(rgb:0x2DC2C5),
- UIColor(rgb:0x5C56F5),
- UIColor(rgb:0x74D12C)
- ]
- )
-
- public static var uiKit = ColorsUIKit(values: values)
- public static var swiftUI = ColorSwiftUI(values: values)
-}
-
-
-
-
-
diff --git a/DesignKit/Variants/Fonts/ElementFonts.swift b/DesignKit/Variants/Fonts/ElementFonts.swift
deleted file mode 100644
index e0a612f85..000000000
--- a/DesignKit/Variants/Fonts/ElementFonts.swift
+++ /dev/null
@@ -1,150 +0,0 @@
-//
-// 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
-
-/// Fonts at https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1362%3A0
-@objcMembers
-public class ElementFonts {
-
- // MARK: - Types
-
- /// A wrapper to provide both a `UIFont` and a SwiftUI `Font` in the same type.
- /// The need for this comes from `Font` not adapting for dynamic type until the app
- /// is restarted (or working at all in Xcode Previews) when initialised from a `UIFont`
- /// (even if that font was created with the appropriate metrics).
- public struct SharedFont {
- public let uiFont: UIFont
- public let font: Font
- }
-
- // MARK: - Setup
-
- public init() {
- }
-
- // MARK: - Private
-
- /// Returns an instance of the font associated with the text style and scaled appropriately for the content size category defined in the trait collection.
- /// Keep this method private method at the moment and create a DesignKit.Fonts.TextStyle if needed.
- fileprivate func font(forTextStyle textStyle: UIFont.TextStyle, compatibleWith traitCollection: UITraitCollection? = nil) -> UIFont {
- return UIFont.preferredFont(forTextStyle: textStyle, compatibleWith: traitCollection)
- }
-}
-
-// MARK: - Fonts protocol
-extension ElementFonts: Fonts {
-
- public var largeTitle: SharedFont {
- let uiFont = self.font(forTextStyle: .largeTitle)
- return SharedFont(uiFont: uiFont, font: .largeTitle)
- }
-
- public var largeTitleB: SharedFont {
- let uiFont = self.largeTitle.uiFont.vc_bold
- return SharedFont(uiFont: uiFont, font: .largeTitle.bold())
- }
-
- public var title1: SharedFont {
- let uiFont = self.font(forTextStyle: .title1)
- return SharedFont(uiFont: uiFont, font: .title)
- }
-
- public var title1B: SharedFont {
- let uiFont = self.title1.uiFont.vc_bold
- return SharedFont(uiFont: uiFont, font: .title.bold())
- }
-
- public var title2: SharedFont {
- let uiFont = self.font(forTextStyle: .title2)
- return SharedFont(uiFont: uiFont, font: .title2)
- }
-
- public var title2B: SharedFont {
- let uiFont = self.title2.uiFont.vc_bold
- return SharedFont(uiFont: uiFont, font: .title2.bold())
- }
-
- public var title3: SharedFont {
- let uiFont = self.font(forTextStyle: .title3)
- return SharedFont(uiFont: uiFont, font: .title3)
- }
-
- public var title3SB: SharedFont {
- let uiFont = self.title3.uiFont.vc_semiBold
- return SharedFont(uiFont: uiFont, font: .title3.weight(.semibold))
- }
-
- public var headline: SharedFont {
- let uiFont = self.font(forTextStyle: .headline)
- return SharedFont(uiFont: uiFont, font: .headline)
- }
-
- public var subheadline: SharedFont {
- let uiFont = self.font(forTextStyle: .subheadline)
- return SharedFont(uiFont: uiFont, font: .subheadline)
- }
-
- public var body: SharedFont {
- let uiFont = self.font(forTextStyle: .body)
- return SharedFont(uiFont: uiFont, font: .body)
- }
-
- public var bodySB: SharedFont {
- let uiFont = self.body.uiFont.vc_semiBold
- return SharedFont(uiFont: uiFont, font: .body.weight(.semibold))
- }
-
- public var callout: SharedFont {
- let uiFont = self.font(forTextStyle: .callout)
- return SharedFont(uiFont: uiFont, font: .callout)
- }
-
- public var calloutSB: SharedFont {
- let uiFont = self.callout.uiFont.vc_semiBold
- return SharedFont(uiFont: uiFont, font: .callout.weight(.semibold))
- }
-
- public var footnote: SharedFont {
- let uiFont = self.font(forTextStyle: .footnote)
- return SharedFont(uiFont: uiFont, font: .footnote)
- }
-
- public var footnoteSB: SharedFont {
- let uiFont = self.footnote.uiFont.vc_semiBold
- return SharedFont(uiFont: uiFont, font: .footnote.weight(.semibold))
- }
-
- public var caption1: SharedFont {
- let uiFont = self.font(forTextStyle: .caption1)
- return SharedFont(uiFont: uiFont, font: .caption)
- }
-
- public var caption1SB: SharedFont {
- let uiFont = self.caption1.uiFont.vc_semiBold
- return SharedFont(uiFont: uiFont, font: .caption.weight(.semibold))
- }
-
- public var caption2: SharedFont {
- let uiFont = self.font(forTextStyle: .caption2)
- return SharedFont(uiFont: uiFont, font: .caption2)
- }
-
- public var caption2SB: SharedFont {
- let uiFont = self.caption2.uiFont.vc_semiBold
- return SharedFont(uiFont: uiFont, font: .caption2.weight(.semibold))
- }
-}
diff --git a/DesignKit/target.yml b/DesignKit/target.yml
deleted file mode 100644
index e10f76f12..000000000
--- a/DesignKit/target.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: DesignKit
-
-schemes:
- DesignKit:
- analyze:
- config: Debug
- archive:
- config: Release
- build:
- targets:
- DesignKit:
- - running
- - profiling
- - analyzing
- - archiving
- profile:
- config: Release
- run:
- config: Debug
- disableMainThreadChecker: true
-
-targets:
- DesignKit:
- type: framework
- platform: iOS
-
- configFiles:
- Debug: Debug.xcconfig
- Release: Release.xcconfig
-
- sources:
- - path: .
- - path: ../Riot/Categories/UIColor.swift
diff --git a/Podfile b/Podfile
index 0a00d2469..1ad092f27 100644
--- a/Podfile
+++ b/Podfile
@@ -16,7 +16,7 @@ use_frameworks!
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
#
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
-$matrixSDKVersion = '= 0.23.10'
+$matrixSDKVersion = '= 0.23.11'
# $matrixSDKVersion = :local
# $matrixSDKVersion = { :branch => 'develop'}
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
@@ -72,6 +72,7 @@ abstract_target 'RiotPods' do
# PostHog for analytics
pod 'PostHog', '~> 1.4.4'
+ pod 'Sentry', '~> 7.15.0'
pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift', :inhibit_warnings => false
# pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec'
diff --git a/Podfile.lock b/Podfile.lock
index 7e05e4ccd..33e1eaaf5 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -87,6 +87,9 @@ PODS:
- Reusable/View (= 4.1.2)
- Reusable/Storyboard (4.1.2)
- Reusable/View (4.1.2)
+ - Sentry (7.15.0):
+ - Sentry/Core (= 7.15.0)
+ - Sentry/Core (7.15.0)
- SideMenu (6.5.0)
- SwiftBase32 (0.9.0)
- SwiftGen (6.5.1)
@@ -126,6 +129,7 @@ DEPENDENCIES:
- PostHog (~> 1.4.4)
- ReadMoreTextView (~> 3.0.1)
- Reusable (~> 4.1)
+ - Sentry (~> 7.15.0)
- SideMenu (~> 6.5)
- SwiftBase32 (~> 0.9.0)
- SwiftGen (~> 6.3)
@@ -169,6 +173,7 @@ SPEC REPOS:
- ReadMoreTextView
- Realm
- Reusable
+ - Sentry
- SideMenu
- SwiftBase32
- SwiftGen
@@ -223,6 +228,7 @@ SPEC CHECKSUMS:
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
Realm: 9ca328bd7e700cc19703799785e37f77d1a130f2
Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136
+ Sentry: 63ca44f5e0c8cea0ee5a07686b02e56104f41ef7
SideMenu: f583187d21c5b1dd04c72002be544b555a2627a2
SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17
SwiftGen: a6d22010845f08fe18fbdf3a07a8e380fd22e0ea
@@ -235,6 +241,6 @@ SPEC CHECKSUMS:
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
-PODFILE CHECKSUM: fdddeaf3f403004b1cc6200add1b6b9e00d40906
+PODFILE CHECKSUM: b3c7c064fc2b74dc937762364faab403fc3fd041
-COCOAPODS: 1.11.2
+COCOAPODS: 1.11.3
diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme
index a9bea1d96..f973b344c 100644
--- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme
+++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme
@@ -1,7 +1,7 @@
+ version = "1.7">
-
-
-
- pinRoomsWithMissedNotif
-
- pinRoomsWithUnread
-
- matrixApps
-
- showAllEventsInRoomHistory
-
- showRedactionsInRoomHistory
-
- showUnsupportedEventsInRoomHistory
-
- sortRoomMembersUsingLastSeenTime
-
- showLeftMembersInRoomMemberList
-
- syncLocalContacts
-
- enableRageShake
-
- maxAllowedMediaCacheSize
- 1073741824
- presenceColorForOnlineUser
- 3401011
- presenceColorForUnavailableUser
- 15066368
- presenceColorForOfflineUser
- 15020851
- enableBotCreation
-
- enableRingingForGroupCalls
-
-
-
diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings
index 92e34f942..b87ff55be 100644
--- a/Riot/Assets/en.lproj/Untranslated.strings
+++ b/Riot/Assets/en.lproj/Untranslated.strings
@@ -88,6 +88,3 @@
"password_validation_error_contain_uppercase_letter" = "Contain an upper-case letter.";
"password_validation_error_contain_number" = "Contain a number.";
"password_validation_error_contain_symbol" = "Contain a symbol.";
-
-// MARK: Room Info
-"room_info_back_button_title" = "Room Info";
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 64b115a23..a32d1b96f 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -1060,6 +1060,8 @@ Tap the + to start adding people.";
"today" = "Today";
"yesterday" = "Yesterday";
"network_offline_prompt" = "The Internet connection appears to be offline.";
+"network_offline_title" = "You're offline";
+"network_offline_message" = "You're offline, check your connection.";
"homeserver_connection_lost" = "Could not connect to the homeserver.";
"public_room_section_title" = "Public Rooms (at %@):";
"bug_report_prompt" = "The application has crashed last time. Would you like to submit a crash report?";
@@ -1822,6 +1824,7 @@ Tap the + to start adding people.";
"room_info_list_one_member" = "1 member";
"room_info_list_several_members" = "%@ members";
"room_info_list_section_other" = "Other";
+"room_info_back_button_title" = "Room Info";
// MARK: - Dial Pad
"dialpad_title" = "Dial pad";
@@ -2164,6 +2167,7 @@ Tap the + to start adding people.";
To enable access, tap Settings> Location and select Always";
"location_sharing_allow_background_location_validate_action" = "Settings";
"location_sharing_allow_background_location_cancel_action" = "Not now";
+"location_sharing_map_credits_title" = "© Copyright";
// MARK: Live location sharing
@@ -2180,7 +2184,7 @@ To enable access, tap Settings> Location and select Always";
"location_sharing_live_list_item_last_update" = "Updated %@ ago";
"location_sharing_live_list_item_last_update_invalid" = "Unknown last update";
"location_sharing_live_list_item_current_user_display_name" = "You";
-"location_sharing_live_list_item_stop_sharing_action" = "Stop sharing";
+"location_sharing_live_list_item_stop_sharing_action" = "Stop";
"location_sharing_live_timer_incoming" = "Live until %@";
"location_sharing_live_loading" = "Loading Live location...";
"location_sharing_live_error" = "Live location error";
@@ -2192,6 +2196,10 @@ To enable access, tap Settings> Location and select Always";
"location_sharing_live_stop_sharing_error" = "Fail to stop sharing location";
"location_sharing_live_stop_sharing_progress" = "Stop location sharing";
+"location_sharing_live_lab_promotion_title" = "Live location sharing";
+"location_sharing_live_lab_promotion_text" = "Please note: this is a labs feature using a temporary implementation that allows the history of your shared location to be permanently visible to other people in the room.";
+"location_sharing_live_lab_promotion_activation" = "Enable live location sharing";
+
// MARK: - MatrixKit
@@ -2340,7 +2348,7 @@ To enable access, tap Settings> Location and select Always";
// Settings
"settings" = "Settings";
-"settings_enable_inapp_notifications" = "Enable In-App notifications";
+"settings_enable_inapp_notifications" = "Enable in-app notifications";
"settings_enable_push_notifications" = "Enable push notifications";
"settings_enter_validation_token_for" = "Enter validation token for %@:";
diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings
index d2afa6093..bfef13906 100644
--- a/Riot/Assets/es.lproj/Vector.strings
+++ b/Riot/Assets/es.lproj/Vector.strings
@@ -2424,3 +2424,20 @@
"location_sharing_allow_background_location_validate_action" = "Ajustes";
"location_sharing_allow_background_location_title" = "Permitir acceso";
"settings_ui_show_redactions_in_room_history" = "Mostrar un indicador donde se haya eliminado un mensaje";
+"settings_timeline" = "LÍNEA DE TIEMPO";
+"room_accessibility_record_voice_message_hint" = "Toca dos veces y mantén para grabar.";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "%@ más";
+"leave_space_selection_no_rooms" = "No seleccionar ninguna sala";
+"leave_space_selection_all_rooms" = "Seleccionar todas las salas";
+"leave_space_selection_title" = "SELECCIONAR SALAS";
+"leave_space_and_more_rooms" = "Salir del espacio y %@ salas";
+"leave_space_and_one_room" = "Salir del espacio y 1 sala";
+
+// Mark: Leave space
+
+"leave_space_action" = "Salir del espacio";
+"home_context_menu_mark_as_read" = "Marcar como leído";
+"room_accessibility_record_voice_message" = "Grabar mensaje de voz";
diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings
index 13ad7e2cb..922bd4580 100644
--- a/Riot/Assets/et.lproj/Vector.strings
+++ b/Riot/Assets/et.lproj/Vector.strings
@@ -2272,3 +2272,21 @@
"location_sharing_allow_background_location_title" = "Luba ligipääs asukohale";
"settings_labs_enable_live_location_sharing" = "Praeguse asukoha jagamine reaalajas (funktsionaalsus on arendamisel ning ajutiselt on asukohad jututoa ajaloos näha)";
"settings_ui_show_redactions_in_room_history" = "Näita kustutatud sõnumite asemel kohatäidet";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "veel %@";
+"leave_space_selection_no_rooms" = "Ära vali ühtegi jututuba";
+"leave_space_selection_all_rooms" = "Vali kõik jututoad";
+"leave_space_selection_title" = "VALI JUTUTUBE";
+"leave_space_and_more_rooms" = "Lahku kogukonnakeskusest ja %@'st jututoast";
+"leave_space_and_one_room" = "Lahku kogukonnakeskusest ja 1'st jututoast";
+
+// Mark: Leave space
+
+"leave_space_action" = "Lahku kogukonnakeskusest";
+"spaces_feature_not_available" = "See funktsionaalsus pole siin rakenduses saadaval. Seni saad vastavat võimalust kasutada %@'i versioonis tavaarvutis.";
+"home_context_menu_mark_as_read" = "Märgi loetuks";
+"settings_timeline" = "AJAJOON";
+"room_accessibility_record_voice_message_hint" = "Salvestamiseks klõpsi kaks korda ja hoia.";
+"room_accessibility_record_voice_message" = "Salvesta häälsõnum";
diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings
index 9310d6f2e..2a3615008 100644
--- a/Riot/Assets/id.lproj/Vector.strings
+++ b/Riot/Assets/id.lproj/Vector.strings
@@ -2530,3 +2530,22 @@
"location_sharing_allow_background_location_message" = "Jika Anda ingin membagikan lokasi langsung Anda, Element membutuhkan akses lokasi ketika aplikasi berada di latar belakang.Untuk memberikan akses, ketuk Pengaturan> Lokasi dan pilih Selalu";
"location_sharing_allow_background_location_title" = "Perbolehkan akses";
"settings_labs_enable_live_location_sharing" = "Pembagian lokasi langsung — bagikan lokasi saat ini (dalam pengembangan aktif, dan sementara, lokasi tetap di riwayat ruangan)";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "%@ lainnya";
+"leave_space_selection_no_rooms" = "Pilih tidak ada ruangan";
+"leave_space_selection_all_rooms" = "Pilih semua ruangan";
+"leave_space_selection_title" = "PILIH RUANGAN";
+"leave_space_and_more_rooms" = "Tinggalkan space dan %@ ruangan";
+"leave_space_and_one_room" = "Tinggalkan space dan 1 ruangan";
+
+// Mark: Leave space
+
+"leave_space_action" = "Tinggalkan space";
+"spaces_feature_not_available" = "Fitur ini tidak tersedia di sini. Untuk sekarang, Anda dapat melakukannya dengan %@ di komputer Anda.";
+"home_context_menu_mark_as_read" = "Tandai sebagai dibaca";
+"settings_ui_show_redactions_in_room_history" = "Tampilkan tampungan untuk pesan yang dihapus";
+"settings_timeline" = "LINIMASA";
+"room_accessibility_record_voice_message_hint" = "Ketuk dua kali dan tekan untuk merekam.";
+"room_accessibility_record_voice_message" = "Rekam Pesan Suara";
diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings
index 0124a1283..7053448e7 100644
--- a/Riot/Assets/it.lproj/Vector.strings
+++ b/Riot/Assets/it.lproj/Vector.strings
@@ -2304,3 +2304,21 @@
"location_sharing_allow_background_location_title" = "Permetti accesso";
"settings_labs_enable_live_location_sharing" = "Condivisione posizione in tempo reale - condividi la posizione attuale (in sviluppo attivo e, per ora, le posizioni restano nella cronologia della stanza)";
"settings_ui_show_redactions_in_room_history" = "Mostra un segnaposto per i messaggi rimossi";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "Altre %@";
+"leave_space_selection_no_rooms" = "Non selezionare alcuna stanza";
+"leave_space_selection_all_rooms" = "Seleziona tutte le stanze";
+"leave_space_selection_title" = "SELEZIONA STANZE";
+"leave_space_and_more_rooms" = "Esci dallo spazio e da %@ stanze";
+"leave_space_and_one_room" = "Esci dallo spazio e da 1 stanza";
+
+// Mark: Leave space
+
+"leave_space_action" = "Esci dallo spazio";
+"spaces_feature_not_available" = "Questa funzionalità non è disponibile qui. Per ora puoi farlo con %@ sul tuo computer.";
+"home_context_menu_mark_as_read" = "Segna come letto";
+"settings_timeline" = "LINEA TEMPORALE";
+"room_accessibility_record_voice_message_hint" = "Doppio tocco e tieni premuto per registrare.";
+"room_accessibility_record_voice_message" = "Registra messaggio vocale";
diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings
index a5421f605..e2a80f7c2 100644
--- a/Riot/Assets/ja.lproj/Vector.strings
+++ b/Riot/Assets/ja.lproj/Vector.strings
@@ -1640,3 +1640,19 @@
"existing" = "既存";
"new_word" = "新規";
"stop" = "停止";
+"spaces_creation_post_process_creating_space_task" = "%@を作成中";
+"side_menu_coach_message" = "右にスワイプまたはタップで全てのルームが表示されます";
+"spaces_creation_post_process_creating_space" = "スペースを作成中";
+"spaces_creation_add_rooms_message" = "このスペースはあなた専用のため、他の人に通知されることはありません。この設定は後から変更できます。";
+"spaces_creation_add_rooms_title" = "どれを追加しますか?";
+"spaces_creation_sharing_type_me_and_teammates_detail" = "あなたとチームメイトの非公開のスペース";
+"spaces_creation_sharing_type_me_and_teammates_title" = "自分とチームメイト";
+"spaces_creation_sharing_type_just_me_detail" = "ルームを整理するための非公開のスペース";
+"spaces_creation_sharing_type_just_me_title" = "自分専用";
+"spaces_creation_sharing_type_message" = "参加者を選択してください%@。この設定は後から変更できます。";
+"spaces_creation_settings_message" = "詳細を入力してください。この設定は後から変更できます。";
+"spaces_creation_address_default_message" = "スペースは以下のように表記されます\n%@";
+"space_settings_current_address_message" = "スペースは以下のように表記されます\n%@";
+"space_topic" = "説明文";
+"spaces_creation_cancel_message" = "進捗状況は失われます。";
+"spaces_creation_cancel_title" = "スペースの作成を停止しますか?";
diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings
index 19c900719..7fc62a33f 100644
--- a/Riot/Assets/nl.lproj/Vector.strings
+++ b/Riot/Assets/nl.lproj/Vector.strings
@@ -2491,3 +2491,21 @@
/* The %@ placeholder will be replaced with the integration manager's URL. */
"settings_integrations_allow_description" = "Gebruik een integratiebeheerder (%@) om bots, bruggen, widgets en stickerpakketten te beheren.\n\nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, kameruitnodigingen versturen en bestuursniveaus instellen namens u.";
"settings_ui_show_redactions_in_room_history" = "Toon een aanduiding voor verwijderde berichten";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "%@ meer";
+"leave_space_selection_no_rooms" = "Selecteer geen kamers";
+"leave_space_selection_all_rooms" = "Selecteer alle kamers";
+"leave_space_selection_title" = "KAMERS KIEZEN";
+"leave_space_and_more_rooms" = "Verlaat space en %@ kamers";
+"leave_space_and_one_room" = "Verlaat space en 1 kamer";
+
+// Mark: Leave space
+
+"leave_space_action" = "Verlaat space";
+"spaces_feature_not_available" = "Deze functie is hier niet beschikbaar. Voor nu kunt u dit doen met %@ op uw computer.";
+"home_context_menu_mark_as_read" = "Markeer als gelezen";
+"settings_timeline" = "TIJDLIJN";
+"room_accessibility_record_voice_message_hint" = "Dubbeltik en houd vast om op te nemen.";
+"room_accessibility_record_voice_message" = "Spraakbericht opnemen";
diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings
index ea0746bf3..e58648ef8 100644
--- a/Riot/Assets/pt_BR.lproj/Vector.strings
+++ b/Riot/Assets/pt_BR.lproj/Vector.strings
@@ -2301,3 +2301,21 @@
"location_sharing_allow_background_location_title" = "Permitir acesso";
"settings_labs_enable_live_location_sharing" = "Compartilhamento de localização ao vivo - compartilhar localização atual (desenvolvimento ativo, e temporariamente, localizações persistem em histórico de sala)";
"settings_ui_show_redactions_in_room_history" = "Mostrar um placeholder para mensagens removidas";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "Mais %@";
+"leave_space_selection_no_rooms" = "Selecionar nenhuma sala";
+"leave_space_selection_all_rooms" = "Selecionar todas as salas";
+"leave_space_selection_title" = "SELECIONAR SALAS";
+"leave_space_and_more_rooms" = "Sair de espaço e %@ salas";
+"leave_space_and_one_room" = "Sair de espaço e 1 sala";
+
+// Mark: Leave space
+
+"leave_space_action" = "Sair de espaço";
+"spaces_feature_not_available" = "Esta funcionalidade não está disponível aqui. Por enquanto, você pode fazer isto com %@ em seu computador.";
+"home_context_menu_mark_as_read" = "Marcar como lida";
+"settings_timeline" = "TIMELINE";
+"room_accessibility_record_voice_message_hint" = "Toque duplo e segure para gravar.";
+"room_accessibility_record_voice_message" = "Gravar Mensagem de Voz";
diff --git a/Riot/Assets/ru.lproj/InfoPlist.strings b/Riot/Assets/ru.lproj/InfoPlist.strings
index 19e148ef6..c3bb844cb 100644
--- a/Riot/Assets/ru.lproj/InfoPlist.strings
+++ b/Riot/Assets/ru.lproj/InfoPlist.strings
@@ -6,3 +6,4 @@
"NSCalendarsUsageDescription" = "Просматривайте запланированные встречи в приложении.";
"NSFaceIDUsageDescription" = "Face ID используется для доступа к вашему приложению.";
"NSLocationWhenInUseUsageDescription" = "Когда вы делитесь с людьми своим местоположением, Element необходим доступ, чтобы показать им карту.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Когда вы сообщаете людям свое местоположение, Element будет необходим доступ, чтобы показать им карту.";
diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings
index eae00e845..e79cfa97a 100644
--- a/Riot/Assets/sk.lproj/Vector.strings
+++ b/Riot/Assets/sk.lproj/Vector.strings
@@ -2527,3 +2527,21 @@
"location_sharing_allow_background_location_title" = "Povoliť prístup";
"settings_labs_enable_live_location_sharing" = "Zdieľanie polohy v reálnom čase - zdieľanie aktuálnej polohy (v aktívnom vývoji a polohy dočasne pretrvávajú v histórii miestnosti)";
"settings_ui_show_redactions_in_room_history" = "Zobrazovať náhrady za odstránené správy";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "%@ viac";
+"leave_space_selection_no_rooms" = "Nevybrať žiadne miestnosti";
+"leave_space_selection_all_rooms" = "Vybrať všetky miestnosti";
+"leave_space_selection_title" = "VYBRAŤ MIESTNOSTI";
+"leave_space_and_more_rooms" = "Opustiť priestor a %@ miestností";
+"leave_space_and_one_room" = "Opustiť priestor a 1 miestnosť";
+
+// Mark: Leave space
+
+"leave_space_action" = "Opustiť priestor";
+"spaces_feature_not_available" = "Táto funkcia tu nie je k dispozícii. Zatiaľ to môžete urobiť pomocou %@ na vašom počítači.";
+"home_context_menu_mark_as_read" = "Označiť ako prečítané";
+"settings_timeline" = "ČASOVÁ OS";
+"room_accessibility_record_voice_message_hint" = "Ak chcete nahrávať, dvakrát ťuknite a podržte.";
+"room_accessibility_record_voice_message" = "Nahrať hlasovú správu";
diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings
index 2fa6e75c8..1847abd92 100644
--- a/Riot/Assets/sv.lproj/Vector.strings
+++ b/Riot/Assets/sv.lproj/Vector.strings
@@ -294,7 +294,7 @@
"settings_add_email_address" = "Lägg till e-postadress";
"settings_phone_number" = "Telefon";
"settings_add_phone_number" = "Lägg till telefonnummer";
-"settings_change_password" = "Byt Matrixkontolösenord";
+"settings_change_password" = "Byt lösenord";
"settings_night_mode" = "Nattläge";
"settings_fail_to_update_profile" = "Misslyckades att uppdatera profil";
"settings_three_pids_management_information_part1" = "Hantera vilka e-postadresser eller telefonnummer som du kan använda för att logga in eller återfå ditt konto här. Kontrollera vilka som kan hitta dig i ";
@@ -325,9 +325,9 @@
"settings_privacy_policy" = "Integritetspolicy";
"settings_send_crash_report" = "Skicka anonyma krasch- och användningsdata";
"settings_enable_rageshake" = "Raseriskaka för att rapportera bugg";
-"settings_old_password" = "gammalt lösenord";
-"settings_new_password" = "nytt lösenord";
-"settings_confirm_password" = "bekräfta lösenord";
+"settings_old_password" = "Gammalt lösenord";
+"settings_new_password" = "Nytt lösenord";
+"settings_confirm_password" = "Bekräfta lösenord";
"settings_fail_to_update_password" = "Misslyckades att uppdatera Matrixkontolösenord";
"settings_password_updated" = "Ditt Matrixkontolösenord har uppdaterats";
"settings_add_3pid_password_title_email" = "Lägg till e-postadress";
@@ -2267,3 +2267,22 @@
"location_sharing_allow_background_location_message" = "Om du vill dela din realtidsplats så behöver Element platsåtkomst när appen är i bakgrunden. För att aktivera åtkomst, gå till Inställningar > Plats och välj Alltid";
"location_sharing_allow_background_location_title" = "Tillåt åtkomst";
"settings_labs_enable_live_location_sharing" = "Platsdelning i realtid - dela nuvarande plats (aktiv utveckling, och för tillfället ligger platser kvar i rumshistoriken)";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "%@ till";
+"leave_space_selection_no_rooms" = "Välj inga rum";
+"leave_space_selection_all_rooms" = "Välj alla rum";
+"leave_space_selection_title" = "VÄLJ RUM";
+"leave_space_and_more_rooms" = "Lämna utrymme och %@ rum";
+"leave_space_and_one_room" = "Lämna utrymme och 1 rum";
+
+// Mark: Leave space
+
+"leave_space_action" = "Lämna utrymme";
+"spaces_feature_not_available" = "Den här funktionen är inte tillgänglig här. För tillfället kan du göra det med %@ på din dator.";
+"home_context_menu_mark_as_read" = "Markera som läst";
+"settings_ui_show_redactions_in_room_history" = "Visa en platshållare för borttagna meddelanden";
+"settings_timeline" = "TIDSLINJE";
+"room_accessibility_record_voice_message_hint" = "Dubbeltryck och håll för att spela in.";
+"room_accessibility_record_voice_message" = "Spela in röstmeddelande";
diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings
index 714cfef89..9c754920a 100644
--- a/Riot/Assets/uk.lproj/Vector.strings
+++ b/Riot/Assets/uk.lproj/Vector.strings
@@ -2529,3 +2529,21 @@
"location_sharing_allow_background_location_title" = "Дозволити доступ";
"settings_labs_enable_live_location_sharing" = "Поширення місцеперебування наживо - діліться поточним розташуванням (в активній розробці, місця тимчасово зберігаються в історії кімнат)";
"settings_ui_show_redactions_in_room_history" = "Показувати заповнювач для вилучених повідомлень";
+
+// MARK: Reactions
+
+"room_event_action_reaction_more" = "Ще %@";
+"leave_space_selection_no_rooms" = "Не вибирати кімнати";
+"leave_space_selection_all_rooms" = "Вибрати всі кімнати";
+"leave_space_selection_title" = "ВИБРАТИ КІМНАТИ";
+"leave_space_and_more_rooms" = "Вийти з простору та %@ кімнат";
+"leave_space_and_one_room" = "Вийти з простору та однієї кімнати";
+
+// Mark: Leave space
+
+"leave_space_action" = "Вийти з простору";
+"spaces_feature_not_available" = "Ця функція тут недоступна. Наразі ви можете це зробити з %@ на своєму комп’ютері.";
+"home_context_menu_mark_as_read" = "Позначити прочитаним";
+"settings_timeline" = "СТРІЧКА";
+"room_accessibility_record_voice_message_hint" = "Двічі торкніться й утримуйте для запису.";
+"room_accessibility_record_voice_message" = "Записати голосове повідомлення";
diff --git a/Riot/Generated/RiotDefaults.swift b/Riot/Generated/RiotDefaults.swift
deleted file mode 100644
index 793d63662..000000000
--- a/Riot/Generated/RiotDefaults.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-// swiftlint:disable all
-// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
-
-import Foundation
-
-// swiftlint:disable superfluous_disable_command
-// swiftlint:disable file_length
-
-// MARK: - Plist Files
-
-// swiftlint:disable identifier_name line_length type_body_length
-internal enum RiotDefaults {
- private static let _document = PlistDocument(path: "Riot-Defaults.plist")
-
- internal static let enableBotCreation: Bool = _document["enableBotCreation"]
- internal static let enableRageShake: Bool = _document["enableRageShake"]
- internal static let enableRingingForGroupCalls: Bool = _document["enableRingingForGroupCalls"]
- internal static let matrixApps: Bool = _document["matrixApps"]
- internal static let maxAllowedMediaCacheSize: Int = _document["maxAllowedMediaCacheSize"]
- internal static let pinRoomsWithMissedNotif: Bool = _document["pinRoomsWithMissedNotif"]
- internal static let pinRoomsWithUnread: Bool = _document["pinRoomsWithUnread"]
- internal static let presenceColorForOfflineUser: Int = _document["presenceColorForOfflineUser"]
- internal static let presenceColorForOnlineUser: Int = _document["presenceColorForOnlineUser"]
- internal static let presenceColorForUnavailableUser: Int = _document["presenceColorForUnavailableUser"]
- internal static let showAllEventsInRoomHistory: Bool = _document["showAllEventsInRoomHistory"]
- internal static let showLeftMembersInRoomMemberList: Bool = _document["showLeftMembersInRoomMemberList"]
- internal static let showRedactionsInRoomHistory: Bool = _document["showRedactionsInRoomHistory"]
- internal static let showUnsupportedEventsInRoomHistory: Bool = _document["showUnsupportedEventsInRoomHistory"]
- internal static let sortRoomMembersUsingLastSeenTime: Bool = _document["sortRoomMembersUsingLastSeenTime"]
- internal static let syncLocalContacts: Bool = _document["syncLocalContacts"]
-}
-// swiftlint:enable identifier_name line_length type_body_length
-
-// MARK: - Implementation Details
-
-private func arrayFromPlist(at path: String) -> [T] {
- guard let url = BundleToken.bundle.url(forResource: path, withExtension: nil),
- let data = NSArray(contentsOf: url) as? [T] else {
- fatalError("Unable to load PLIST at path: \(path)")
- }
- return data
-}
-
-private struct PlistDocument {
- let data: [String: Any]
-
- init(path: String) {
- guard let url = BundleToken.bundle.url(forResource: path, withExtension: nil),
- let data = NSDictionary(contentsOf: url) as? [String: Any] else {
- fatalError("Unable to load PLIST at path: \(path)")
- }
- self.data = data
- }
-
- subscript(key: String) -> T {
- guard let result = data[key] as? T else {
- fatalError("Property '\(key)' is not of type \(T.self)")
- }
- return result
- }
-}
-
-// swiftlint:disable convenience_type
-private final class BundleToken {
- static let bundle: Bundle = {
- #if SWIFT_PACKAGE
- return Bundle.module
- #else
- return Bundle(for: BundleToken.self)
- #endif
- }()
-}
-// swiftlint:enable convenience_type
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index a40347d7c..f90273a30 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -2815,6 +2815,18 @@ public class VectorL10n: NSObject {
public static var locationSharingLiveError: String {
return VectorL10n.tr("Vector", "location_sharing_live_error")
}
+ /// Enable live location sharing
+ public static var locationSharingLiveLabPromotionActivation: String {
+ return VectorL10n.tr("Vector", "location_sharing_live_lab_promotion_activation")
+ }
+ /// Please note: this is a labs feature using a temporary implementation that allows the history of your shared location to be permanently visible to other people in the room.
+ public static var locationSharingLiveLabPromotionText: String {
+ return VectorL10n.tr("Vector", "location_sharing_live_lab_promotion_text")
+ }
+ /// Live location sharing
+ public static var locationSharingLiveLabPromotionTitle: String {
+ return VectorL10n.tr("Vector", "location_sharing_live_lab_promotion_title")
+ }
/// You
public static var locationSharingLiveListItemCurrentUserDisplayName: String {
return VectorL10n.tr("Vector", "location_sharing_live_list_item_current_user_display_name")
@@ -2831,7 +2843,7 @@ public class VectorL10n: NSObject {
public static var locationSharingLiveListItemSharingExpired: String {
return VectorL10n.tr("Vector", "location_sharing_live_list_item_sharing_expired")
}
- /// Stop sharing
+ /// Stop
public static var locationSharingLiveListItemStopSharingAction: String {
return VectorL10n.tr("Vector", "location_sharing_live_list_item_stop_sharing_action")
}
@@ -2895,6 +2907,10 @@ public class VectorL10n: NSObject {
public static func locationSharingLocatingUserErrorTitle(_ p1: String) -> String {
return VectorL10n.tr("Vector", "location_sharing_locating_user_error_title", p1)
}
+ /// © Copyright
+ public static var locationSharingMapCreditsTitle: String {
+ return VectorL10n.tr("Vector", "location_sharing_map_credits_title")
+ }
/// Open in Apple Maps
public static var locationSharingOpenAppleMaps: String {
return VectorL10n.tr("Vector", "location_sharing_open_apple_maps")
@@ -3239,10 +3255,18 @@ public class VectorL10n: NSObject {
public static var networkErrorNotReachable: String {
return VectorL10n.tr("Vector", "network_error_not_reachable")
}
+ /// You're offline, check your connection.
+ public static var networkOfflineMessage: String {
+ return VectorL10n.tr("Vector", "network_offline_message")
+ }
/// The Internet connection appears to be offline.
public static var networkOfflinePrompt: String {
return VectorL10n.tr("Vector", "network_offline_prompt")
}
+ /// You're offline
+ public static var networkOfflineTitle: String {
+ return VectorL10n.tr("Vector", "network_offline_title")
+ }
/// New
public static var newWord: String {
return VectorL10n.tr("Vector", "new_word")
@@ -5231,6 +5255,10 @@ public class VectorL10n: NSObject {
public static var roomEventFailedToSend: String {
return VectorL10n.tr("Vector", "room_event_failed_to_send")
}
+ /// Room Info
+ public static var roomInfoBackButtonTitle: String {
+ return VectorL10n.tr("Vector", "room_info_back_button_title")
+ }
/// 1 member
public static var roomInfoListOneMember: String {
return VectorL10n.tr("Vector", "room_info_list_one_member")
@@ -6723,7 +6751,7 @@ public class VectorL10n: NSObject {
public static var settingsEnableCallkit: String {
return VectorL10n.tr("Vector", "settings_enable_callkit")
}
- /// Enable In-App notifications
+ /// Enable in-app notifications
public static var settingsEnableInappNotifications: String {
return VectorL10n.tr("Vector", "settings_enable_inapp_notifications")
}
diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift
index 6e22234a3..f69a82fbc 100644
--- a/Riot/Generated/UntranslatedStrings.swift
+++ b/Riot/Generated/UntranslatedStrings.swift
@@ -234,10 +234,6 @@ public extension VectorL10n {
static var passwordValidationInfoHeader: String {
return VectorL10n.tr("Untranslated", "password_validation_info_header")
}
- /// Room Info
- static var roomInfoBackButtonTitle: String {
- return VectorL10n.tr("Untranslated", "room_info_back_button_title")
- }
}
// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
diff --git a/Riot/Managers/PushNotification/PushNotificationService.m b/Riot/Managers/PushNotification/PushNotificationService.m
index 27b07292a..58b9aaaf9 100644
--- a/Riot/Managers/PushNotification/PushNotificationService.m
+++ b/Riot/Managers/PushNotification/PushNotificationService.m
@@ -335,7 +335,7 @@ Matrix session observer used to detect new opened sessions.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
NSDictionary *userInfo = notification.request.content.userInfo;
- if (userInfo[Constants.userInfoKeyPresentNotificationOnForeground])
+ if (RiotSettings.shared.showInAppNotifications || userInfo[Constants.userInfoKeyPresentNotificationOnForeground])
{
if (!userInfo[Constants.userInfoKeyPresentNotificationInRoom]
&& [[AppDelegate theDelegate].visibleRoomId isEqualToString:userInfo[@"room_id"]])
@@ -347,7 +347,7 @@ Matrix session observer used to detect new opened sessions.
{
completionHandler(UNNotificationPresentationOptionBadge
| UNNotificationPresentationOptionSound
- | UNNotificationPresentationOptionAlert);
+ | UNNotificationPresentationOptionBanner);
}
}
else
diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift
index 634919814..548fad9c7 100644
--- a/Riot/Managers/Settings/RiotSettings.swift
+++ b/Riot/Managers/Settings/RiotSettings.swift
@@ -80,6 +80,10 @@ final class RiotSettings: NSObject {
return RiotSettings.defaults.object(forKey: UserDefaultsKeys.notificationsShowDecryptedContent) != nil
}
+ /// Indicate if notifications should be shown whilst the app is in the foreground.
+ @UserDefault(key: "showInAppNotifications", defaultValue: true, storage: defaults)
+ var showInAppNotifications
+
/// Indicate if encrypted messages content should be displayed in notifications.
@UserDefault(key: UserDefaultsKeys.notificationsShowDecryptedContent, defaultValue: false, storage: defaults)
var showDecryptedContentInNotifications
@@ -154,7 +158,11 @@ final class RiotSettings: NSObject {
/// Indicates if live location sharing is enabled
@UserDefault(key: UserDefaultsKeys.enableLiveLocationSharing, defaultValue: false, storage: defaults)
- var enableLiveLocationSharing
+ var enableLiveLocationSharing {
+ didSet {
+ NotificationCenter.default.post(name: RiotSettings.didUpdateLiveLocationSharingActivation, object: self)
+ }
+ }
// MARK: Calls
@@ -375,3 +383,8 @@ final class RiotSettings: NSObject {
@UserDefault(key: "lastNumberOfTrackedSpaces", defaultValue: nil, storage: defaults)
var lastNumberOfTrackedSpaces: Int?
}
+
+// MARK: - RiotSettings notification constants
+extension RiotSettings {
+ public static let didUpdateLiveLocationSharingActivation = Notification.Name("RiotSettingsDidUpdateLiveLocationSharingActivation")
+}
diff --git a/Riot/Managers/Theme/ElementUIColorsResolved.swift b/Riot/Managers/Theme/ElementUIColorsResolved.swift
new file mode 100644
index 000000000..20118bfd3
--- /dev/null
+++ b/Riot/Managers/Theme/ElementUIColorsResolved.swift
@@ -0,0 +1,82 @@
+//
+// 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 DesignTokens
+
+extension UIColor {
+ /// The colors from DesignKit, resolved for light mode only.
+ static let elementLight = ElementUIColorsResolved(dynamicColors: element, userInterfaceStyle: .light)
+ /// The colors from DesignKit, resolved for dark mode only.
+ static let elementDark = ElementUIColorsResolved(dynamicColors: element, userInterfaceStyle: .dark)
+}
+
+/// The dynamic colors from DesignKit, resolved to light or dark mode for use in the UIKit themes.
+///
+/// As Element doesn't (currently) update the app's `UIUserInterfaceStyle` when selecting
+/// a custom theme, the dynamic colors provided by DesignKit need resolving for each theme to
+/// prevent them from respecting the interface style and rendering in the wrong style.
+@objcMembers public class ElementUIColorsResolved: NSObject {
+ // MARK: Compound
+ public let accent: UIColor
+ public let alert: UIColor
+ public let primaryContent: UIColor
+ public let secondaryContent: UIColor
+ public let tertiaryContent: UIColor
+ public let quaternaryContent: UIColor
+ public let quinaryContent: UIColor
+ public let system: UIColor
+ public let background: UIColor
+
+ public let namesAndAvatars: [UIColor]
+
+ // MARK: Legacy
+ public let quarterlyContent: UIColor
+ public let navigation: UIColor
+ public let tile: UIColor
+ public let separator: UIColor
+
+ // MARK: Setup
+ public init(dynamicColors: ElementUIColors, userInterfaceStyle: UIUserInterfaceStyle) {
+ let traitCollection = UITraitCollection(userInterfaceStyle: userInterfaceStyle)
+
+ self.accent = dynamicColors.accent.resolvedColor(with: traitCollection)
+ self.alert = dynamicColors.alert.resolvedColor(with: traitCollection)
+ self.primaryContent = dynamicColors.primaryContent.resolvedColor(with: traitCollection)
+ self.secondaryContent = dynamicColors.secondaryContent.resolvedColor(with: traitCollection)
+ self.tertiaryContent = dynamicColors.tertiaryContent.resolvedColor(with: traitCollection)
+ self.quaternaryContent = dynamicColors.quaternaryContent.resolvedColor(with: traitCollection)
+ self.quinaryContent = dynamicColors.quinaryContent.resolvedColor(with: traitCollection)
+ self.system = dynamicColors.system.resolvedColor(with: traitCollection)
+ self.background = dynamicColors.background.resolvedColor(with: traitCollection)
+
+ self.namesAndAvatars = dynamicColors.contentAndAvatars
+
+ // Legacy colours
+ self.quarterlyContent = dynamicColors.quaternaryContent.resolvedColor(with: traitCollection)
+ self.navigation = dynamicColors.system.resolvedColor(with: traitCollection)
+
+ if userInterfaceStyle == .light {
+ self.tile = UIColor(rgb: 0xF3F8FD)
+ self.separator = dynamicColors.quinaryContent.resolvedColor(with: traitCollection)
+ } else {
+ self.tile = dynamicColors.quinaryContent.resolvedColor(with: traitCollection)
+ self.separator = dynamicColors.system.resolvedColor(with: traitCollection)
+ }
+
+ super.init()
+ }
+}
diff --git a/DesignKit/Source/ThemeV2.swift b/Riot/Managers/Theme/ThemeV2.swift
similarity index 70%
rename from DesignKit/Source/ThemeV2.swift
rename to Riot/Managers/Theme/ThemeV2.swift
index dedc4d6df..56a97a93a 100644
--- a/DesignKit/Source/ThemeV2.swift
+++ b/Riot/Managers/Theme/ThemeV2.swift
@@ -14,29 +14,18 @@
// limitations under the License.
//
-import Foundation
import UIKit
+import DesignKit
+import DesignTokens
/// Theme v2. May be named again as `Theme` when the migration completed.
@objc public protocol ThemeV2 {
/// Colors object
- var colors: ColorsUIKit { get }
+ var colors: ElementUIColorsResolved { get }
/// Fonts object
- var fonts: FontsUIKit { get }
-
- /// may contain more design components in future, like icons, audio files etc.
-}
-
-/// Theme v2 for SwiftUI.
-public protocol ThemeSwiftUIType {
-
- /// Colors object
- var colors: ColorSwiftUI { get }
-
- /// Fonts object
- var fonts: FontSwiftUI { get }
+ var fonts: ElementUIFonts { get }
/// may contain more design components in future, like icons, audio files etc.
}
diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift
index 84b0239ce..388105356 100644
--- a/Riot/Managers/Theme/Themes/DarkTheme.swift
+++ b/Riot/Managers/Theme/Themes/DarkTheme.swift
@@ -181,9 +181,9 @@ class DarkTheme: NSObject, Theme {
button.setTitleColor(self.tintColor, for: .normal)
}
- /// MARK: - Theme v2
- var colors: ColorsUIKit = DarkColors.uiKit
+ // MARK: - Theme v2
+ var colors = UIColor.elementDark
- var fonts: FontsUIKit = FontsUIKit(values: ElementFonts())
+ var fonts = UIFont.element
}
diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift
index a07de1e8f..7ec7101c8 100644
--- a/Riot/Managers/Theme/Themes/DefaultTheme.swift
+++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift
@@ -14,7 +14,6 @@
limitations under the License.
*/
-import Foundation
import UIKit
import DesignKit
@@ -186,8 +185,8 @@ class DefaultTheme: NSObject, Theme {
button.setTitleColor(self.tintColor, for: .normal)
}
- /// MARK: - Theme v2
- var colors: ColorsUIKit = LightColors.uiKit
+ // MARK: - Theme v2
+ var colors = UIColor.elementLight
- var fonts: FontsUIKit = FontsUIKit(values: ElementFonts())
+ var fonts = UIFont.element
}
diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift
index 9505b5620..030b4463c 100644
--- a/Riot/Modules/Analytics/Analytics.swift
+++ b/Riot/Modules/Analytics/Analytics.swift
@@ -17,8 +17,12 @@
import PostHog
import AnalyticsEvents
-/// A class responsible for managing an analytics client
-/// and sending events through this client.
+/// A class responsible for managing a variety of analytics clients
+/// and sending events through these clients.
+///
+/// Events may include user activity, or app health data such as crashes,
+/// non-fatal issues and performance. `Analytics` class serves as a façade
+/// to all these use cases.
///
/// ## Creating Analytics Events
///
@@ -42,6 +46,9 @@ import AnalyticsEvents
/// The analytics client to send events with.
private var client: AnalyticsClientProtocol = PostHogAnalyticsClient()
+ /// The monitoring client to track crashes, issues and performance
+ private var monitoringClient = SentryMonitoringClient()
+
/// The service used to interact with account data settings.
private var service: AnalyticsService?
@@ -106,6 +113,7 @@ import AnalyticsEvents
// The order is important here. PostHog ignores the reset if stopped.
reset()
client.stop()
+ monitoringClient.stop()
MXLog.debug("[Analytics] Stopped.")
}
@@ -115,6 +123,7 @@ import AnalyticsEvents
guard RiotSettings.shared.enableAnalytics, !isRunning else { return }
client.start()
+ monitoringClient.start()
// Sanity check in case something went wrong.
guard client.isRunning else { return }
@@ -163,6 +172,7 @@ import AnalyticsEvents
/// Note: **MUST** be called before stopping PostHog or the reset is ignored.
func reset() {
client.reset()
+ monitoringClient.reset()
MXLog.debug("[Analytics] Reset.")
RiotSettings.shared.isIdentifiedForAnalytics = false
@@ -374,4 +384,7 @@ extension Analytics: MXAnalyticsDelegate {
capture(event: event)
}
+ func trackNonFatalIssue(_ issue: String, details: [String : Any]?) {
+ monitoringClient.trackNonFatalIssue(issue, details: details)
+ }
}
diff --git a/Riot/Modules/Analytics/SentryMonitoringClient.swift b/Riot/Modules/Analytics/SentryMonitoringClient.swift
new file mode 100644
index 000000000..32b2169f2
--- /dev/null
+++ b/Riot/Modules/Analytics/SentryMonitoringClient.swift
@@ -0,0 +1,67 @@
+//
+// 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 Sentry
+import MatrixSDK
+
+/// Sentry client used as part of the Analytics set of tools to track health metrics
+/// of the application, such as crashes, non-fatal issues and performance.
+///
+/// All analytics tracking, incl. health metrics, is subject to user consent,
+/// configurable in user settings.
+struct SentryMonitoringClient {
+ private static let sentryDSN = "https://a5e37731f9b94642a1b93093cacbee4c@sentry.tools.element.io/47"
+
+ func start() {
+ guard !SentrySDK.isEnabled else { return }
+
+ MXLog.debug("[SentryMonitoringClient] Started")
+ SentrySDK.start { options in
+ options.dsn = Self.sentryDSN
+ options.tracesSampleRate = 1.0
+
+ options.beforeSend = { event in
+ MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)")
+ return event
+ }
+
+ options.onCrashedLastRun = { event in
+ MXLog.debug("[SentryMonitoringClient] Last run crashed: \(event)")
+ }
+ }
+ }
+
+ func stop() {
+ MXLog.debug("[SentryMonitoringClient] Stopped")
+ SentrySDK.close()
+ }
+
+ func reset() {
+ MXLog.debug("[SentryMonitoringClient] Reset")
+ SentrySDK.startSession()
+ }
+
+ func trackNonFatalIssue(_ issue: String, details: [String: Any]?) {
+ guard SentrySDK.isEnabled else { return }
+
+ let event = Event()
+ event.level = .error
+ event.message = .init(formatted: issue)
+ event.extra = details
+ SentrySDK.capture(event: event)
+ }
+}
diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift
index 82aae24c0..f380cdc44 100755
--- a/Riot/Modules/Application/AppCoordinator.swift
+++ b/Riot/Modules/Application/AppCoordinator.swift
@@ -94,6 +94,16 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
self.addSideMenu()
}
+ NotificationCenter.default.addObserver(forName: NSNotification.Name.appDelegateNetworkStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] notification in
+ guard let self = self else { return }
+
+ if AppDelegate.theDelegate().isOffline {
+ self.splitViewCoordinator?.showAppStateIndicator(with: VectorL10n.networkOfflineTitle, icon: UIImage(systemName: "wifi.slash"))
+ } else {
+ self.splitViewCoordinator?.hideAppStateIndicator()
+ }
+ }
+
// NOTE: When split view is shown there can be no Matrix sessions ready. Keep this behavior or use a loading screen before showing the split view.
self.showSplitView()
MXLog.debug("[AppCoordinator] Showed split view")
diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m
index b592ee311..a5fe2bd43 100644
--- a/Riot/Modules/Application/LegacyAppDelegate.m
+++ b/Riot/Modules/Application/LegacyAppDelegate.m
@@ -195,8 +195,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
UIView *launchAnimationContainerView;
}
-@property (strong, nonatomic) UIAlertController *mxInAppNotification;
-
@property (strong, nonatomic) UIAlertController *logoutConfirmation;
@property (weak, nonatomic) UIAlertController *gdprConsentNotGivenAlertController;
@@ -587,13 +585,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Remove expired URL previews from the cache
[URLPreviewService.shared removeExpiredCacheData];
- // Hide potential notification
- if (self.mxInAppNotification)
- {
- [self.mxInAppNotification dismissViewControllerAnimated:NO completion:nil];
- self.mxInAppNotification = nil;
- }
-
// Discard any process on pending universal link
[self resetPendingUniversalLink];
@@ -1321,8 +1312,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Sanity check
if (!pathParams.count)
{
- MXLogDebug(@"[AppDelegate] Universal link: Error: No path parameters");
- return NO;
+ // Handle simple room links with aliases/identifiers as UniversalLink will not parse these.
+ NSString* absoluteUrl = [universalLink.url.absoluteString stringByRemovingPercentEncoding];
+ if ([MXTools isMatrixRoomAlias:absoluteUrl]
+ || [MXTools isMatrixRoomIdentifier:absoluteUrl])
+ {
+ pathParams = @[absoluteUrl];
+ }
+ else {
+ MXLogDebug(@"[AppDelegate] Universal link: Error: No path parameters");
+ return NO;
+ }
}
NSString *roomIdOrAlias;
@@ -1820,18 +1820,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// start the call service
[self.callPresenter start];
- // Look for the account related to this session.
- NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts;
- for (MXKAccount *account in mxAccounts)
- {
- if (account.mxSession == mxSession)
- {
- // Enable inApp notifications (if they are allowed for this account).
- [self enableInAppNotificationsForAccount:account];
- break;
- }
- }
-
[self.configuration setupSettingsWhenLoadedFor:mxSession];
// Register to user new device sign in notification
@@ -1888,9 +1876,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Set up push notifications
[self.pushNotificationService registerUserNotificationSettings];
}
-
- // Observe inApp notifications toggle change
- [account addObserver:self forKeyPath:@"enableInAppNotifications" options:0 context:nil];
}
[self.delegate legacyAppDelegate:self didAddAccount:account];
@@ -1901,10 +1886,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Remove inApp notifications toggle change
MXKAccount *account = notif.object;
- if (!account.isSoftLogout)
- {
- [account removeObserver:self forKeyPath:@"enableInAppNotifications"];
- }
// Clear Modular data
[[WidgetManager sharedManager] deleteDataForUser:account.mxCredentials.userId];
@@ -1984,12 +1965,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Set up push notifications
[self.pushNotificationService registerUserNotificationSettings];
-
- // Observe inApp notifications toggle change for each account
- for (MXKAccount *account in mxAccounts)
- {
- [account addObserver:self forKeyPath:@"enableInAppNotifications" options:0 context:nil];
- }
}
}
@@ -2256,10 +2231,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Flush and restore Matrix data
[self reloadMatrixSessions:NO];
}
- else if ([@"enableInAppNotifications" isEqualToString:keyPath] && [object isKindOfClass:[MXKAccount class]])
- {
- [self enableInAppNotificationsForAccount:(MXKAccount*)object];
- }
else if (object == [MXKAppSettings standardAppSettings] && [keyPath isEqualToString:@"enableCallKit"])
{
BOOL isCallKitEnabled = [MXKAppSettings standardAppSettings].isCallKitEnabled;
@@ -2656,100 +2627,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
#pragma mark - Matrix Accounts handling
-- (void)enableInAppNotificationsForAccount:(MXKAccount*)account
-{
- if (account.mxSession)
- {
- if (account.enableInAppNotifications)
- {
- // Build MXEvent -> NSString formatter
- EventFormatter *eventFormatter = [[EventFormatter alloc] initWithMatrixSession:account.mxSession];
- eventFormatter.isForSubtitle = YES;
-
- [account listenToNotifications:^(MXEvent *event, MXRoomState *roomState, MXPushRule *rule) {
-
- // Check conditions to display this notification
- if (![self.visibleRoomId isEqualToString:event.roomId]
- && !self.window.rootViewController.presentedViewController)
- {
- MXKEventFormatterError error;
- NSString* messageText = [eventFormatter stringFromEvent:event
- withRoomState:roomState
- andLatestRoomState:nil
- error:&error];
- if (messageText.length && (error == MXKEventFormatterErrorNone))
- {
- // Removing existing notification (if any)
- if (self.mxInAppNotification)
- {
- [self.mxInAppNotification dismissViewControllerAnimated:NO completion:nil];
- }
-
- // Check whether tweak is required
- for (MXPushRuleAction *ruleAction in rule.actions)
- {
- if (ruleAction.actionType == MXPushRuleActionTypeSetTweak)
- {
- if ([[ruleAction.parameters valueForKey:@"set_tweak"] isEqualToString:@"sound"])
- {
- // Play message sound
- AudioServicesPlaySystemSound(self->_messageSound);
- }
- }
- }
-
- MXRoomSummary *roomSummary = [account.mxSession roomSummaryWithRoomId:event.roomId];
-
- __weak typeof(self) weakSelf = self;
- self.mxInAppNotification = [UIAlertController alertControllerWithTitle:roomSummary.displayname
- message:messageText
- preferredStyle:UIAlertControllerStyleAlert];
-
- [self.mxInAppNotification addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
- style:UIAlertActionStyleCancel
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self.mxInAppNotification = nil;
- [account updateNotificationListenerForRoomId:event.roomId ignore:YES];
- }
-
- }]];
-
- [self.mxInAppNotification addAction:[UIAlertAction actionWithTitle:[VectorL10n view]
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self.mxInAppNotification = nil;
- // Show the room
- [self showRoom:event.roomId andEventId:nil withMatrixSession:account.mxSession];
- }
-
- }]];
-
- [self.window.rootViewController presentViewController:self.mxInAppNotification animated:YES completion:nil];
- }
- }
- }];
- }
- else
- {
- [account removeNotificationListener];
- }
- }
-
- if (self.mxInAppNotification)
- {
- [self.mxInAppNotification dismissViewControllerAnimated:NO completion:nil];
- self.mxInAppNotification = nil;
- }
-}
-
- (void)selectMatrixAccount:(void (^)(MXKAccount *selectedAccount))onSelection
{
NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts;
@@ -4375,16 +4252,24 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
- (void)setupUserDefaults
{
- // Register "Riot-Defaults.plist" default values
- NSString* userDefaults = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UserDefaults"];
- NSString *defaultsPathFromApp = [[NSBundle mainBundle] pathForResource:userDefaults ofType:@"plist"];
- NSMutableDictionary *defaults = [[NSDictionary dictionaryWithContentsOfFile:defaultsPathFromApp] mutableCopy];
-
- // add pusher ids, as they don't belong to plist anymore
- defaults[@"pushKitAppIdProd"] = BuildSettings.pushKitAppIdProd;
- defaults[@"pushKitAppIdDev"] = BuildSettings.pushKitAppIdDev;
- defaults[@"pusherAppIdProd"] = BuildSettings.pusherAppIdProd;
- defaults[@"pusherAppIdDev"] = BuildSettings.pusherAppIdDev;
+ // Register MatrixKit defaults.
+ NSDictionary *defaults = @{
+ @"enableBotCreation": @(BuildSettings.enableBotCreation),
+ @"maxAllowedMediaCacheSize": @(BuildSettings.maxAllowedMediaCacheSize),
+ @"presenceColorForOfflineUser": @(BuildSettings.presenceColorForOfflineUser),
+ @"presenceColorForOnlineUser": @(BuildSettings.presenceColorForOnlineUser),
+ @"presenceColorForUnavailableUser": @(BuildSettings.presenceColorForUnavailableUser),
+ @"showAllEventsInRoomHistory": @(BuildSettings.showAllEventsInRoomHistory),
+ @"showLeftMembersInRoomMemberList": @(BuildSettings.showLeftMembersInRoomMemberList),
+ @"showRedactionsInRoomHistory": @(BuildSettings.showRedactionsInRoomHistory),
+ @"showUnsupportedEventsInRoomHistory": @(BuildSettings.showUnsupportedEventsInRoomHistory),
+ @"sortRoomMembersUsingLastSeenTime": @(BuildSettings.syncLocalContacts),
+ @"syncLocalContacts": @(BuildSettings.syncLocalContacts),
+ @"pushKitAppIdProd": BuildSettings.pushKitAppIdProd,
+ @"pushKitAppIdDev": BuildSettings.pushKitAppIdDev,
+ @"pusherAppIdProd": BuildSettings.pusherAppIdProd,
+ @"pusherAppIdDev": BuildSettings.pusherAppIdDev
+ };
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingBottomSheetPreferences.swift b/Riot/Modules/Common/SwiftUI/VectorHostingBottomSheetPreferences.swift
new file mode 100644
index 000000000..cb5afa51a
--- /dev/null
+++ b/Riot/Modules/Common/SwiftUI/VectorHostingBottomSheetPreferences.swift
@@ -0,0 +1,121 @@
+//
+// 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
+
+/// `VectorHostingBottomSheetPreferences` defines the bottom sheet behaviour using the `UISheetPresentationController` of the `UIViewController`
+class VectorHostingBottomSheetPreferences {
+
+ // MARK: - Detent
+
+ enum Detent {
+ case medium
+ case large
+
+ @available(iOS 15, *)
+ fileprivate func uiSheetDetent() -> UISheetPresentationController.Detent {
+ switch self {
+ case .medium: return .medium()
+ case .large: return .large()
+ }
+ }
+
+ @available(iOS 15, *)
+ fileprivate func uiSheetDetentId() -> UISheetPresentationController.Detent.Identifier {
+ switch self {
+ case .medium: return .medium
+ case .large: return .large
+ }
+ }
+ }
+
+ // MARK: - Public
+
+ // The array of detents that the sheet may rest at.
+ // This array must have at least one element.
+ // Detents must be specified in order from smallest to largest height.
+ // Default: [.medium, .large]
+ let detents: [Detent]
+
+ // The default detent. When nil or the identifier is not found in detents, the sheet is displayed at the smallest detent.
+ // Default: nil
+ let defaultDetent: Detent?
+
+ // The largest detent that is not dimmed. When nil or the identifier is not found in detents, all detents are dimmed.
+ // Default: nil
+ let largestUndimmedDetent: Detent?
+ let cornerRadius: CGFloat?
+
+ // If there is a larger detent to expand to than the selected detent, and a descendent scroll view is scrolled to top, this controls whether scrolling down will expand to a larger detent.
+ // Useful to set to NO for non-modal sheets, where scrolling in the sheet should not expand the sheet and obscure the content above.
+ // Default: YES
+ let prefersScrollingExpandsWhenScrolledToEdge: Bool
+
+ // Set to YES to show a grabber at the top of the sheet.
+ // Default: `nil` -> the grabber is shown if more than one detent is configured
+ let prefersGrabberVisible: Bool?
+
+ // MARK: - Setup
+
+ init(detents: [Detent] = [.medium, .large],
+ defaultDetent: Detent? = nil,
+ largestUndimmedDetent: Detent? = nil,
+ prefersGrabberVisible: Bool? = nil,
+ cornerRadius: CGFloat? = nil,
+ prefersScrollingExpandsWhenScrolledToEdge: Bool = true) {
+ self.detents = detents
+ self.defaultDetent = defaultDetent
+ self.largestUndimmedDetent = largestUndimmedDetent
+ self.prefersGrabberVisible = prefersGrabberVisible
+ self.cornerRadius = cornerRadius
+ self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
+ }
+
+ // MARK: - Public
+
+ func setup(viewController: UIViewController) {
+ guard #available(iOS 15.0, *) else { return }
+
+ guard let sheetController = viewController.sheetPresentationController else {
+ MXLog.debug("[VectorHostingBottomSheetPreferences] setup: no sheetPresentationController found")
+ return
+ }
+
+ sheetController.detents = self.uiSheetDetents()
+ if let prefersGrabberVisible = self.prefersGrabberVisible {
+ sheetController.prefersGrabberVisible = prefersGrabberVisible
+ } else {
+ sheetController.prefersGrabberVisible = self.detents.count > 1
+ }
+ sheetController.selectedDetentIdentifier = self.defaultDetent?.uiSheetDetentId()
+ sheetController.largestUndimmedDetentIdentifier = self.largestUndimmedDetent?.uiSheetDetentId()
+ sheetController.prefersScrollingExpandsWhenScrolledToEdge = self.prefersScrollingExpandsWhenScrolledToEdge
+ if let cornerRadius = self.cornerRadius {
+ sheetController.preferredCornerRadius = cornerRadius
+ }
+ }
+
+ // MARK: - Private
+
+ @available(iOS 15, *)
+ fileprivate func uiSheetDetents() -> [UISheetPresentationController.Detent] {
+ var uiSheetDetents: [UISheetPresentationController.Detent] = []
+ for detent in detents {
+ uiSheetDetents.append(detent.uiSheetDetent())
+ }
+ return uiSheetDetents
+ }
+}
diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
index c636350b1..9b6860fb7 100644
--- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
+++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
@@ -25,23 +25,34 @@ class VectorHostingController: UIHostingController {
// MARK: Private
- var isNavigationBarHidden: Bool = false
- var hidesBackTitleWhenPushed: Bool = false
private var theme: Theme
// MARK: Public
-
+
+ /// Wether or not the navigation bar should be hidden. Default `false`
+ var isNavigationBarHidden: Bool = false
+ /// Wether or not the title of the back item should be hidden. Default `false`
+ var hidesBackTitleWhenPushed: Bool = false
+ /// Defines the behaviour of the `VectorHostingController` as a bottom sheet. Default `nil`
+ var bottomSheetPreferences: VectorHostingBottomSheetPreferences?
+
/// Whether or not to use the iOS 15 style scroll edge appearance when the controller has a navigation bar.
var enableNavigationBarScrollEdgeAppearance = false
/// When non-nil, the style will be applied to the status bar.
var statusBarStyle: UIStatusBarStyle?
+
+ private let forceZeroSafeAreaInsets: Bool
override var preferredStatusBarStyle: UIStatusBarStyle {
statusBarStyle ?? super.preferredStatusBarStyle
}
-
- init(rootView: Content) where Content: View {
+ /// Initializer
+ /// - Parameter rootView: Root view for the controller.
+ /// - Parameter forceZeroSafeAreaInsets: Whether to force-set the hosting view's safe area insets to zero. Useful when the view is used as part of a table view.
+ init(rootView: Content,
+ forceZeroSafeAreaInsets: Bool = false) where Content: View {
self.theme = ThemeService.shared().theme
+ self.forceZeroSafeAreaInsets = forceZeroSafeAreaInsets
super.init(rootView: AnyView(rootView.vectorContent()))
}
@@ -58,6 +69,8 @@ class VectorHostingController: UIHostingController {
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
+
+ bottomSheetPreferences?.setup(viewController: self)
}
override func viewWillAppear(_ animated: Bool) {
@@ -88,6 +101,22 @@ class VectorHostingController: UIHostingController {
self.view.invalidateIntrinsicContentSize()
}
}
+
+ override func viewSafeAreaInsetsDidChange() {
+ super.viewSafeAreaInsetsDidChange()
+
+ guard forceZeroSafeAreaInsets else {
+ return
+ }
+
+ let counterSafeAreaInsets = UIEdgeInsets(top: -view.safeAreaInsets.top,
+ left: -view.safeAreaInsets.left,
+ bottom: -view.safeAreaInsets.bottom,
+ right: -view.safeAreaInsets.right)
+ if additionalSafeAreaInsets != counterSafeAreaInsets, counterSafeAreaInsets != .zero {
+ additionalSafeAreaInsets = counterSafeAreaInsets
+ }
+ }
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
@@ -98,6 +127,9 @@ class VectorHostingController: UIHostingController {
}
private func update(theme: Theme) {
+ // Ensure dynamic colors are shown correctly when the theme is the opposite appearance to the system.
+ overrideUserInterfaceStyle = theme.userInterfaceStyle
+
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar, withModernScrollEdgeAppearance: enableNavigationBarScrollEdgeAppearance)
}
diff --git a/Riot/Modules/Common/Toasts/RoundedToastView.swift b/Riot/Modules/Common/Toasts/RoundedToastView.swift
index 4774eadd0..879a31167 100644
--- a/Riot/Modules/Common/Toasts/RoundedToastView.swift
+++ b/Riot/Modules/Common/Toasts/RoundedToastView.swift
@@ -96,7 +96,7 @@ class RoundedToastView: UIView, Themable {
}
func update(theme: Theme) {
- backgroundColor = theme.colors.background
+ backgroundColor = theme.colors.system
stackView.arrangedSubviews.first?.tintColor = theme.colors.primaryContent
label.font = theme.fonts.subheadline
label.textColor = theme.colors.primaryContent
@@ -115,6 +115,12 @@ class RoundedToastView: UIView, Themable {
case .success:
imageView.image = Asset.Images.checkmark.image
return imageView
+ case .failure:
+ imageView.image = Asset.Images.errorIcon.image
+ return imageView
+ case .custom(let icon):
+ imageView.image = icon?.withRenderingMode(.alwaysTemplate)
+ return imageView
}
}
}
diff --git a/Riot/Modules/Common/Toasts/ToastViewState.swift b/Riot/Modules/Common/Toasts/ToastViewState.swift
index e241d4645..c3117b549 100644
--- a/Riot/Modules/Common/Toasts/ToastViewState.swift
+++ b/Riot/Modules/Common/Toasts/ToastViewState.swift
@@ -20,6 +20,8 @@ struct ToastViewState {
enum Style {
case loading
case success
+ case failure
+ case custom(icon: UIImage?)
}
let style: Style
diff --git a/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift b/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift
index 36c7c0c6e..a2603e478 100644
--- a/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift
+++ b/Riot/Modules/Common/UserIndicators/UserIndicatorPresenter.swift
@@ -23,6 +23,8 @@ import UIKit
enum UserIndicatorType {
case loading(label: String, isInteractionBlocking: Bool)
case success(label: String)
+ case failure(label: String)
+ case custom(label: String, icon: UIImage?)
}
/// A presenter which can handle `UserIndicatorType` by creating the underlying `UserIndicator`
@@ -75,6 +77,10 @@ class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol {
}
case .success(let label):
return successRequest(label: label)
+ case .failure(let label):
+ return failureRequest(label: label)
+ case .custom(let label, let icon):
+ return customRequest(label: label, icon: icon)
}
}
@@ -116,4 +122,32 @@ class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol {
dismissal: .timeout(1.5)
)
}
+
+ private func failureRequest(label: String) -> UserIndicatorRequest {
+ let presenter = ToastViewPresenter(
+ viewState: .init(
+ style: .failure,
+ label: label
+ ),
+ presentationContext: presentationContext
+ )
+ return UserIndicatorRequest(
+ presenter: presenter,
+ dismissal: .timeout(1.5)
+ )
+ }
+
+ private func customRequest(label: String, icon: UIImage?) -> UserIndicatorRequest {
+ let presenter = ToastViewPresenter(
+ viewState: .init(
+ style: .custom(icon: icon),
+ label: label
+ ),
+ presentationContext: presentationContext
+ )
+ return UserIndicatorRequest(
+ presenter: presenter,
+ dismissal: .manual
+ )
+ }
}
diff --git a/Riot/Modules/Common/UserIndicators/UserIndicatorStore.swift b/Riot/Modules/Common/UserIndicators/UserIndicatorStore.swift
index c6bf3ef8b..204edcb66 100644
--- a/Riot/Modules/Common/UserIndicators/UserIndicatorStore.swift
+++ b/Riot/Modules/Common/UserIndicators/UserIndicatorStore.swift
@@ -24,6 +24,13 @@ import CommonKit
private let presenter: UserIndicatorTypePresenterProtocol
private var indicators: [UserIndicator]
+ @objc init(from viewController: UIViewController) {
+ self.presenter = UserIndicatorTypePresenter(presentingViewController: viewController)
+ self.indicators = []
+
+ super.init()
+ }
+
init(presenter: UserIndicatorTypePresenterProtocol) {
self.presenter = presenter
self.indicators = []
@@ -59,4 +66,24 @@ import CommonKit
let indicator = presenter.present(.success(label: label))
indicators.append(indicator)
}
+
+ /// Present an error message that will be automatically dismissed after a few seconds.
+ ///
+ /// Note: This is a convenience function callable by objective-c code
+ @objc func presentFailure(label: String) {
+ let indicator = presenter.present(.failure(label: label))
+ indicators.append(indicator)
+ }
+
+ /// Present an custom message
+ /// To remove the indicator call the returned `UserIndicatorCancel` function
+ ///
+ /// Note: This is a convenience function callable by objective-c code
+ @objc func presentCustom(label: String, icon: UIImage?) -> UserIndicatorCancel {
+ let indicator = presenter.present(.custom(label: label, icon: icon))
+ indicators.append(indicator)
+ return {
+ indicator.cancel()
+ }
+ }
}
diff --git a/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift b/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift
index a54e37a41..08b3080f7 100644
--- a/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift
+++ b/Riot/Modules/Common/UserIndicators/ViewPresenters/ToastViewPresenter.swift
@@ -23,7 +23,7 @@ import MatrixSDK
/// It is managed by an `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes.
class ToastViewPresenter: UserIndicatorViewPresentable {
struct Constants {
- static let navigationBarPatting = CGFloat(10)
+ static let navigationBarPatting = CGFloat(12)
}
private let viewState: ToastViewState
@@ -50,7 +50,7 @@ class ToastViewPresenter: UserIndicatorViewPresentable {
navigation.view.addSubview(view)
NSLayoutConstraint.activate([
view.centerXAnchor.constraint(equalTo: navigation.view.centerXAnchor),
- view.topAnchor.constraint(equalTo: navigation.navigationBar.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.navigationBarPatting)
+ view.topAnchor.constraint(equalTo: navigation.navigationBar.safeAreaLayoutGuide.bottomAnchor, constant: Constants.navigationBarPatting)
])
} else {
viewController.view.addSubview(view)
diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m
index 9f97d97fb..d56614b0a 100644
--- a/Riot/Modules/Contacts/ContactsTableViewController.m
+++ b/Riot/Modules/Contacts/ContactsTableViewController.m
@@ -115,6 +115,15 @@
}];
[self userInterfaceThemeDidChange];
+
+ if (@available(iOS 15.0, *))
+ {
+ [[_contactsTableView.bottomAnchor constraintEqualToAnchor:self.view.keyboardLayoutGuide.topAnchor] setActive:YES];
+ }
+ else
+ {
+ [[_contactsTableView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] setActive:YES];
+ }
}
- (void)userInterfaceThemeDidChange
diff --git a/Riot/Modules/Contacts/ContactsTableViewController.xib b/Riot/Modules/Contacts/ContactsTableViewController.xib
index 6d29ed62d..75cdfdcab 100644
--- a/Riot/Modules/Contacts/ContactsTableViewController.xib
+++ b/Riot/Modules/Contacts/ContactsTableViewController.xib
@@ -1,11 +1,9 @@
-
-
-
-
+
+
-
+
@@ -21,8 +19,8 @@
-
-
+
+
@@ -32,15 +30,14 @@
+
-
-
-
+
diff --git a/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift b/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift
index d18c9a991..e5111eacc 100644
--- a/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift
+++ b/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift
@@ -21,7 +21,7 @@ final class EncryptionKeysExportPresenter: NSObject {
// MARK: - Constants
private enum Constants {
- static let keyExportFileName = "riot-keys.txt"
+ static let keyExportFileName = "element-keys.txt"
}
// MARK: - Properties
diff --git a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift
index c68e4f358..c56d0fc0e 100644
--- a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift
+++ b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewController.swift
@@ -17,6 +17,7 @@
*/
import UIKit
+import MatrixSDK
final class KeyVerificationVerifyByScanningViewController: UIViewController {
@@ -215,7 +216,12 @@ final class KeyVerificationVerifyByScanningViewController: UIViewController {
private func qrCodeImage(from data: Data) -> UIImage? {
let codeGenerator = QRCodeGenerator()
- return codeGenerator.generateCode(from: data, with: self.codeImageView.frame.size)
+ do {
+ return try codeGenerator.generateCode(from: data, with: codeImageView.frame.size)
+ } catch {
+ MXLog.error("[KeyVerificationVerifyByScanningViewController] qrCodeImage: cannot generate QR code - \(error)")
+ return nil
+ }
}
private func presentQRCodeReader(animated: Bool) {
diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift
index a04a5d2b3..fb568f5af 100644
--- a/Riot/Modules/LocationSharing/LocationManager.swift
+++ b/Riot/Modules/LocationSharing/LocationManager.swift
@@ -112,7 +112,7 @@ class LocationManager: NSObject {
switch accuracy {
case .full:
- self.locationManager.startUpdatingLocation()
+ self.locationManager.stopUpdatingLocation()
case .reduced:
self.locationManager.stopMonitoringSignificantLocationChanges()
}
diff --git a/Riot/Modules/LocationSharing/UserLocationServiceProvider.swift b/Riot/Modules/LocationSharing/UserLocationServiceProvider.swift
index a9230b181..3ce1dcf60 100644
--- a/Riot/Modules/LocationSharing/UserLocationServiceProvider.swift
+++ b/Riot/Modules/LocationSharing/UserLocationServiceProvider.swift
@@ -26,16 +26,16 @@ class UserLocationServiceProvider {
// MARK: - Properties
+ // UserLocationService per user id
private var locationServices: [String: UserLocationServiceProtocol] = [:]
// MARK: - Setup
private init() {
- guard RiotSettings.shared.enableLiveLocationSharing else {
- return
- }
+ self.setupOrTeardownLocationServices()
- self.registerUserSessionsServiceNotifications()
+ // Listen to lab flag changes
+ self.registerRiotSettingsNotifications()
}
// MARK: - Public
@@ -71,7 +71,7 @@ class UserLocationServiceProvider {
MXLog.debug("Start monitoring user live location sharing")
}
- func setupUserLocationServiceIfNeeded(for userSession: UserSession) {
+ private func setupUserLocationServiceIfNeeded(for userSession: UserSession) {
// Be sure Matrix session has is store setup to access beacon info summaries
guard userSession.matrixSession.state.rawValue >= MXSessionState.storeDataReady.rawValue else {
@@ -100,6 +100,30 @@ class UserLocationServiceProvider {
MXLog.debug("Stop monitoring user live location sharing")
}
+ private func setupOrTeardownLocationServices() {
+
+ self.unregisterUserSessionsServiceNotifications()
+
+ if RiotSettings.shared.enableLiveLocationSharing {
+ self.setupUserLocationServiceForAllUsers()
+ self.registerUserSessionsServiceNotifications()
+ } else {
+ self.tearDownUserLocationServiceForAllUsers()
+ }
+ }
+
+ private func setupUserLocationServiceForAllUsers() {
+ for userSession in UserSessionsService.shared.userSessions {
+ self.setupUserLocationService(for: userSession)
+ }
+ }
+
+ private func tearDownUserLocationServiceForAllUsers() {
+ for (userId, _) in self.locationServices {
+ self.tearDownUserLocationService(for: userId)
+ }
+ }
+
// MARK: UserSessions management
private func registerUserSessionsServiceNotifications() {
@@ -111,6 +135,15 @@ class UserLocationServiceProvider {
NotificationCenter.default.addObserver(self, selector: #selector(userSessionsServiceDidRemoveUserSession(_:)), name: UserSessionsService.didRemoveUserSession, object: nil)
}
+ private func unregisterUserSessionsServiceNotifications() {
+
+ NotificationCenter.default.removeObserver(self, name: UserSessionsService.didAddUserSession, object: nil)
+
+ NotificationCenter.default.removeObserver(self, name: UserSessionsService.userSessionDidChange, object: nil)
+
+ NotificationCenter.default.removeObserver(self, name: UserSessionsService.didRemoveUserSession, object: nil)
+ }
+
@objc private func userSessionsServiceDidAddUserSession(_ notification: Notification) {
guard let userInfo = notification.userInfo, let userSession = userInfo[UserSessionsService.NotificationUserInfoKey.userSession] as? UserSession else {
@@ -137,4 +170,17 @@ class UserLocationServiceProvider {
self.tearDownUserLocationService(for: userId)
}
+
+ // MARK: - RiotSettings
+
+ private func registerRiotSettingsNotifications() {
+
+ NotificationCenter.default.addObserver(self, selector: #selector(riotSettingsDidUpdateLiveLocationSharingActivation(_:)), name: RiotSettings.didUpdateLiveLocationSharingActivation, object: nil)
+ }
+
+ @objc private func riotSettingsDidUpdateLiveLocationSharingActivation(_ notification: Notification) {
+
+ // Lab flag value has changed, check if we should enable or disable location services
+ self.setupOrTeardownLocationServices()
+ }
}
diff --git a/Riot/Modules/MatrixKit/Utils/ErrorPresentation/MXKErrorPresentableBuilder.m b/Riot/Modules/MatrixKit/Utils/ErrorPresentation/MXKErrorPresentableBuilder.m
index cd8a978b4..077fb7001 100644
--- a/Riot/Modules/MatrixKit/Utils/ErrorPresentation/MXKErrorPresentableBuilder.m
+++ b/Riot/Modules/MatrixKit/Utils/ErrorPresentation/MXKErrorPresentableBuilder.m
@@ -31,17 +31,28 @@
return nil;
}
- NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
- NSString *message = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
-
- if (!title)
+ NSString *title;
+ NSString *message;
+
+ if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorNotConnectedToInternet)
{
- title = [VectorL10n error];
+ title = [VectorL10n networkOfflineTitle];
+ message = [VectorL10n networkOfflineMessage];
}
-
- if (!message)
+ else
{
- message = [VectorL10n errorCommonMessage];
+ title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
+ message = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
+
+ if (!title)
+ {
+ title = [VectorL10n error];
+ }
+
+ if (!message)
+ {
+ message = [VectorL10n errorCommonMessage];
+ }
}
return [[MXKErrorViewModel alloc] initWithTitle:title message:message];
diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
index 77a8a0d39..a4b7a8f3b 100644
--- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
+++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
@@ -1356,9 +1356,22 @@ static NSString *const kHTMLATagRegexPattern = @"(
}
else if ([msgtype isEqualToString:kMXMessageTypeFile])
{
- body = body? body : [VectorL10n noticeFileAttachment];
// Check attachment validity
- if (![self isSupportedAttachment:event])
+ if ([self isSupportedAttachment:event])
+ {
+ body = body? body : [VectorL10n noticeFileAttachment];
+
+ NSDictionary *fileInfo = contentToUse[@"info"];
+ if (fileInfo)
+ {
+ NSNumber *fileSize = fileInfo[@"size"];
+ if (fileSize)
+ {
+ body = [NSString stringWithFormat:@"%@ (%@)", body, [MXTools fileSizeToString: fileSize.longValue]];
+ }
+ }
+ }
+ else
{
MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description);
body = [VectorL10n noticeInvalidAttachment];
diff --git a/Riot/Modules/Pills/PillsFormatter.swift b/Riot/Modules/Pills/PillsFormatter.swift
index cdf13ebd8..ccee48317 100644
--- a/Riot/Modules/Pills/PillsFormatter.swift
+++ b/Riot/Modules/Pills/PillsFormatter.swift
@@ -25,6 +25,14 @@ class PillsFormatter: NSObject {
/// UTType identifier for pills. Should be declared as Document type & Exported type identifier inside Info.plist
static let pillUTType: String = "im.vector.app.pills"
+ // MARK: - Internal Enums
+ /// Defines a replacement mode for converting Pills to plain text.
+ @objc enum PillsReplacementTextMode: Int {
+ case displayname
+ case identifier
+ case markdown
+ }
+
// MARK: - Internal Methods
/// Insert text attachments for pills inside given message attributed string.
///
@@ -66,15 +74,24 @@ class PillsFormatter: NSObject {
///
/// - Parameters:
/// - attributedString: attributed string with pills
- /// - asMarkdown: wether pill should be replaced by markdown links or raw text
+ /// - mode: replacement mode for pills (default: displayname)
/// - Returns: string with display names
- static func stringByReplacingPills(in attributedString: NSAttributedString, asMarkdown: Bool = false) -> String {
+ static func stringByReplacingPills(in attributedString: NSAttributedString,
+ mode: PillsReplacementTextMode = .displayname) -> String {
let newAttr = NSMutableAttributedString(attributedString: attributedString)
newAttr.vc_enumerateAttribute(.attachment) { (attachment: PillTextAttachment, range: NSRange, _) in
if let displayText = attachment.data?.displayText,
let userId = attachment.data?.matrixItemId,
let permalink = MXTools.permalinkToUser(withUserId: userId) {
- let pillString = asMarkdown ? "[\(displayText)](\(permalink))" : "\(displayText)"
+ let pillString: String
+ switch mode {
+ case .displayname:
+ pillString = displayText
+ case .identifier:
+ pillString = userId
+ case .markdown:
+ pillString = "[\(displayText)](\(permalink))"
+ }
newAttr.replaceCharacters(in: range, with: pillString)
}
}
diff --git a/Riot/Modules/QRCode/QRCodeGenerator.swift b/Riot/Modules/QRCode/QRCodeGenerator.swift
index f3ad32c05..4ac8e6f6e 100644
--- a/Riot/Modules/QRCode/QRCodeGenerator.swift
+++ b/Riot/Modules/QRCode/QRCodeGenerator.swift
@@ -15,36 +15,29 @@
*/
import Foundation
+import ZXingObjC
final class QRCodeGenerator {
-
- // MARK: - Constants
-
- private enum Constants {
- static let qrCodeGeneratorFilter = "CIQRCodeGenerator"
- static let qrCodeInputCorrectionLevel = "M"
+ enum Error: Swift.Error {
+ case cannotCreateImage
}
- // MARK: - Public
-
- func generateCode(from data: Data, with size: CGSize) -> UIImage? {
- guard let filter = CIFilter(name: Constants.qrCodeGeneratorFilter) else {
- return nil
+ func generateCode(from data: Data, with size: CGSize) throws -> UIImage {
+ let writer = ZXMultiFormatWriter()
+ let endodedString = String(data: data, encoding: .isoLatin1)
+ let scale = UIScreen.main.scale
+ let bitMatrix = try writer.encode(
+ endodedString,
+ format: kBarcodeFormatQRCode,
+ width: Int32(size.width * scale),
+ height: Int32(size.height * scale),
+ hints: ZXEncodeHints()
+ )
+
+ guard let cgImage = ZXImage(matrix: bitMatrix).cgimage else {
+ throw Error.cannotCreateImage
}
- filter.setValue(data, forKey: "inputMessage")
- filter.setValue(Constants.qrCodeInputCorrectionLevel, forKey: "inputCorrectionLevel") // Be sure to use same error resilience level as other platform
-
- guard let ciImage = filter.outputImage else {
- return nil
- }
-
- let scaleX = size.width/ciImage.extent.size.width
- let scaleY = size.height/ciImage.extent.size.height
-
- let transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
-
- let transformedCIImage = ciImage.transformed(by: transform)
- return UIImage(ciImage: transformedCIImage)
+ return UIImage(cgImage: cgImage)
}
}
diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.swift b/Riot/Modules/Room/DataSources/RoomDataSource.swift
index 1fa660bc9..f57896b62 100644
--- a/Riot/Modules/Room/DataSources/RoomDataSource.swift
+++ b/Riot/Modules/Room/DataSources/RoomDataSource.swift
@@ -219,7 +219,7 @@ private extension RoomDataSource {
func htmlMessageFromSanitizedAttributedText(_ sanitizedText: NSAttributedString) -> String? {
let rawText: String
if #available(iOS 15.0, *) {
- rawText = PillsFormatter.stringByReplacingPills(in: sanitizedText, asMarkdown: true)
+ rawText = PillsFormatter.stringByReplacingPills(in: sanitizedText, mode: .markdown)
} else {
rawText = sanitizedText.string
}
diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift
index cd290a412..4e59799e7 100644
--- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift
+++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift
@@ -49,7 +49,7 @@ struct TimelineLiveLocationViewData {
}
var showMap: Bool {
- guard case .started(_, _) = status else {
+ guard case .started = status else {
return false
}
return true
@@ -200,7 +200,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
}
liveLocationContainerView.isHidden = false
- liveLocationContainerView.backgroundColor = theme.colors.background.withAlphaComponent(0.75)
+ liveLocationContainerView.backgroundColor = theme.colors.background.withAlphaComponent(0.90)
liveLocationIcon.image = Asset.Images.locationLiveCellIcon.image
liveLocationIcon.tintColor = bannerViewData.iconTint
diff --git a/Riot/Modules/Room/MXKRoomViewController.h b/Riot/Modules/Room/MXKRoomViewController.h
index 17aa34a37..0ff875fc3 100644
--- a/Riot/Modules/Room/MXKRoomViewController.h
+++ b/Riot/Modules/Room/MXKRoomViewController.h
@@ -394,7 +394,7 @@ typedef NS_ENUM(NSUInteger, MXKRoomViewControllerJoinRoomResult) {
@param string to analyse
@return YES if IRC style command has been detected and interpreted.
*/
-- (BOOL)isIRCStyleCommand:(NSString*)string;
+- (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string;
/**
Mention the member display name in the current text of the message composer.
diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m
index 17c4c2b5e..814977644 100644
--- a/Riot/Modules/Room/MXKRoomViewController.m
+++ b/Riot/Modules/Room/MXKRoomViewController.m
@@ -1250,7 +1250,7 @@
customEventDetailsViewClass = eventDetailsViewClass;
}
-- (BOOL)isIRCStyleCommand:(NSString*)string
+- (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string
{
// Check whether the provided text may be an IRC-style command
if ([string hasPrefix:@"/"] == NO || [string hasPrefix:@"//"] == YES)
@@ -3375,7 +3375,7 @@
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendTextMessage:(NSString*)textMessage
{
// Handle potential IRC commands in typed string
- if ([self isIRCStyleCommand:textMessage] == NO)
+ if ([self sendAsIRCStyleCommandIfPossible:textMessage] == NO)
{
// Send text message in the current room
[self sendTextMessage:textMessage];
diff --git a/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift
index d4939f94c..008b504ee 100644
--- a/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift
+++ b/Riot/Modules/Room/ParticipantsInviteModal/ContactsPicker/ContactsPickerViewModel.swift
@@ -126,6 +126,7 @@ class ContactsPickerViewModel: NSObject, ContactsPickerViewModelProtocol {
contactsViewController.showSearch(true)
contactsViewController.searchBar.placeholder = VectorL10n.roomParticipantsInviteAnotherUser
+ contactsViewController.searchBar.resignFirstResponder()
// Apply the search pattern if any
if currentSearchText != nil {
diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m
index 693f22a9f..26dd78547 100644
--- a/Riot/Modules/Room/RoomViewController.m
+++ b/Riot/Modules/Room/RoomViewController.m
@@ -1228,7 +1228,7 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
-- (BOOL)isIRCStyleCommand:(NSString*)string
+- (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string
{
// Override the default behavior for `/join` command in order to open automatically the joined room
@@ -1271,7 +1271,7 @@ static CGSize kThreadListBarButtonItemImageSize;
}
return YES;
}
- return [super isIRCStyleCommand:string];
+ return [super sendAsIRCStyleCommandIfPossible:string];
}
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
@@ -4801,7 +4801,28 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendAttributedTextMessage:(NSAttributedString *)attributedTextMessage
{
- [self sendAttributedTextMessage:attributedTextMessage];
+ BOOL isMessageAHandledCommand = NO;
+ // "/me" command is supported with Pills in RoomDataSource.
+ if (![attributedTextMessage.string hasPrefix:kMXKSlashCmdEmote])
+ {
+ // Other commands currently work with identifiers (e.g. ban, invite, op, etc).
+ NSString *message;
+ if (@available(iOS 15.0, *))
+ {
+ message = [PillsFormatter stringByReplacingPillsIn:attributedTextMessage mode:PillsReplacementTextModeIdentifier];
+ }
+ else
+ {
+ message = attributedTextMessage.string;
+ }
+ // Try to send the slash command
+ isMessageAHandledCommand = [self sendAsIRCStyleCommandIfPossible:message];
+ }
+
+ if (!isMessageAHandledCommand)
+ {
+ [self sendAttributedTextMessage:attributedTextMessage];
+ }
}
#pragma mark - MXKRoomMemberDetailsViewControllerDelegate
@@ -5512,8 +5533,7 @@ static CGSize kThreadListBarButtonItemImageSize;
}
else if ([AppDelegate theDelegate].isOffline)
{
- self.activitiesViewExpanded = YES;
- [roomActivitiesView displayNetworkErrorNotification:[VectorL10n roomOfflineNotification]];
+ // Doing nothing here as the offline notification is now handled by the AppCoordinator
}
else if (self.customizedRoomDataSource.roomState.isObsolete)
{
@@ -6820,7 +6840,8 @@ static CGSize kThreadListBarButtonItemImageSize;
{
if (@available(iOS 15.0, *))
{
- MXKPasteboardManager.shared.pasteboard.string = [PillsFormatter stringByReplacingPillsIn:attributedTextMessage asMarkdown:YES];
+ MXKPasteboardManager.shared.pasteboard.string = [PillsFormatter stringByReplacingPillsIn:attributedTextMessage
+ mode:PillsReplacementTextModeMarkdown];
}
else
{
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailBaseBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailBaseBubbleCell.swift
index 40b485fa0..83c1b2885 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailBaseBubbleCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailBaseBubbleCell.swift
@@ -22,8 +22,11 @@ class FileWithoutThumbnailBaseBubbleCell: SizableBaseRoomCell, RoomCellReactions
override func render(_ cellData: MXKCellData!) {
super.render(cellData)
-
- self.fileAttachementView?.titleLabel.attributedText = self.suitableAttributedTextMessage
+
+ let attributedText = NSMutableAttributedString(attributedString: self.suitableAttributedTextMessage)
+ attributedText.addAttributes([.foregroundColor: ThemeService.shared().theme.colors.secondaryContent],
+ range: NSRange(location: 0, length: attributedText.length))
+ self.fileAttachementView?.titleLabel.attributedText = attributedText
self.update(theme: ThemeService.shared().theme)
}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.swift
index 1d2058a27..0ca97d522 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.swift
@@ -23,9 +23,10 @@ final class FileWithoutThumbnailCellContentView: UIView, NibLoadable {
// MARK: Outlets
+ @IBOutlet private weak var iconBackgroundView: UIView!
@IBOutlet private weak var iconImageView: UIImageView!
@IBOutlet private(set) weak var titleLabel: UILabel!
-
+
// MARK: Public
var badgeImage: UIImage? {
@@ -49,17 +50,23 @@ final class FileWithoutThumbnailCellContentView: UIView, NibLoadable {
super.awakeFromNib()
self.layer.masksToBounds = true
+ self.iconImageView.image = Asset.Images.fileAttachment.image.withRenderingMode(.alwaysTemplate)
+ self.iconBackgroundView.layer.masksToBounds = true
+
+ update(theme: ThemeService.shared().theme)
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = BubbleRoomCellLayoutConstants.bubbleCornerRadius
+ self.iconBackgroundView.layer.cornerRadius = self.iconBackgroundView.bounds.midX
}
// MARK: - Public
func update(theme: Theme) {
- self.titleLabel.textColor = theme.textPrimaryColor
+ self.iconBackgroundView.backgroundColor = theme.roomCellIncomingBubbleBackgroundColor
+ self.iconImageView.tintColor = theme.colors.secondaryContent
}
}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.xib b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.xib
index 21e33aef7..ebd9e1299 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.xib
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -11,43 +11,56 @@
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailPlainCell.swift
new file mode 100644
index 000000000..8dddf30d2
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailPlainCell.swift
@@ -0,0 +1,75 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class FileWithoutThumbnailPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable, RoomCellThreadSummaryDisplayable {
+
+ private(set) var fileAttachementView: FileWithoutThumbnailCellContentView!
+
+ override func render(_ cellData: MXKCellData!) {
+ super.render(cellData)
+
+ guard let data = cellData as? RoomBubbleCellData else {
+ return
+ }
+
+ guard data.attachment.type == .file else {
+ fatalError("Invalid attachment type passed to a file without thumbnail cell.")
+ }
+
+ let attributedText = NSMutableAttributedString(attributedString: self.suitableAttributedTextMessage)
+ attributedText.addAttributes([.foregroundColor: ThemeService.shared().theme.colors.secondaryContent],
+ range: NSRange(location: 0, length: attributedText.length))
+ self.fileAttachementView.titleLabel.attributedText = attributedText
+
+ self.update(theme: ThemeService.shared().theme)
+ }
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showSenderInfo = true
+ roomCellContentView?.showPaginationTitle = false
+
+ guard let contentView = roomCellContentView?.innerContentView else {
+ return
+ }
+
+ fileAttachementView = FileWithoutThumbnailCellContentView.loadFromNib()
+ contentView.vc_addSubViewMatchingParent(fileAttachementView)
+ }
+
+ override func update(theme: Theme) {
+ super.update(theme: theme)
+
+ guard let fileAttachementView = fileAttachementView else {
+ return
+ }
+
+ fileAttachementView.update(theme: theme)
+ fileAttachementView.backgroundColor = theme.colors.quinaryContent
+ }
+
+ override func onContentViewTap(_ sender: UITapGestureRecognizer!) {
+
+ if let bubbleData = self.bubbleData, bubbleData.isAttachment {
+ self.delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnAttachmentView, userInfo: nil)
+ } else {
+ super.onContentViewTap(sender)
+ }
+ }
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailWithPaginationTitlePlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailWithPaginationTitlePlainCell.swift
new file mode 100644
index 000000000..2fb5b512e
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailWithPaginationTitlePlainCell.swift
@@ -0,0 +1,25 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class FileWithoutThumbnailWithPaginationTitlePlainCell: FileWithoutThumbnailPlainCell {
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showPaginationTitle = true
+ }
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailWithoutSenderInfoPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailWithoutSenderInfoPlainCell.swift
new file mode 100644
index 000000000..810813ff2
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileWithoutThumbnail/FileWithoutThumbnailWithoutSenderInfoPlainCell.swift
@@ -0,0 +1,25 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class FileWithoutThumbnailWithoutSenderInfoPlainCell: FileWithoutThumbnailPlainCell {
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showSenderInfo = false
+ }
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
index 466a1d665..f016761dd 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
@@ -110,7 +110,9 @@
[self registerPollCellsForTableView:tableView];
[self registerLocationCellsForTableView:tableView];
-
+
+ [self registerFileWithoutThumbnailCellsForTableView:tableView];
+
[tableView registerClass:RoomEmptyBubbleCell.class forCellReuseIdentifier:RoomEmptyBubbleCell.defaultReuseIdentifier];
[tableView registerClass:RoomSelectedStickerBubbleCell.class forCellReuseIdentifier:RoomSelectedStickerBubbleCell.defaultReuseIdentifier];
@@ -261,6 +263,13 @@
[tableView registerClass:LocationWithPaginationTitlePlainCell.class forCellReuseIdentifier:LocationWithPaginationTitlePlainCell.defaultReuseIdentifier];
}
+- (void)registerFileWithoutThumbnailCellsForTableView:(UITableView*)tableView
+{
+ [tableView registerClass:FileWithoutThumbnailPlainCell.class forCellReuseIdentifier:FileWithoutThumbnailPlainCell.defaultReuseIdentifier];
+ [tableView registerClass:FileWithoutThumbnailWithoutSenderInfoPlainCell.class forCellReuseIdentifier:FileWithoutThumbnailWithoutSenderInfoPlainCell.defaultReuseIdentifier];
+ [tableView registerClass:FileWithoutThumbnailWithPaginationTitlePlainCell.class forCellReuseIdentifier:FileWithoutThumbnailWithPaginationTitlePlainCell.defaultReuseIdentifier];
+}
+
#pragma mark Cell class association
- (NSDictionary*)buildCellClasses
@@ -435,13 +444,13 @@
{
return @{
// Clear
- @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail) : RoomIncomingTextMsgBubbleCell.class,
- @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo) : RoomIncomingTextMsgWithoutSenderInfoBubbleCell.class,
- @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle) : RoomIncomingTextMsgWithPaginationTitleBubbleCell.class,
+ @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail) : FileWithoutThumbnailPlainCell.class,
+ @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo) : FileWithoutThumbnailWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle) : FileWithoutThumbnailWithPaginationTitlePlainCell.class,
// Encrypted
- @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted) : RoomIncomingEncryptedTextMsgBubbleCell.class,
- @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.class,
- @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.class
+ @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted) : FileWithoutThumbnailPlainCell.class,
+ @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : FileWithoutThumbnailWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : FileWithoutThumbnailWithPaginationTitlePlainCell.class
};
}
@@ -449,13 +458,13 @@
{
return @{
// Clear
- @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail) : RoomOutgoingTextMsgBubbleCell.class,
- @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo) : RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class,
- @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle) : RoomOutgoingTextMsgWithPaginationTitleBubbleCell.class,
+ @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail) : FileWithoutThumbnailWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo) : FileWithoutThumbnailWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle) : FileWithoutThumbnailWithPaginationTitlePlainCell.class,
// Encrypted
- @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted) : RoomOutgoingEncryptedTextMsgBubbleCell.class,
- @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class,
- @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.class
+ @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted) : FileWithoutThumbnailWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : FileWithoutThumbnailWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : FileWithoutThumbnailWithPaginationTitlePlainCell.class
};
}
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift
index c3abeb01f..b5fae41d8 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift
@@ -137,7 +137,7 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable {
}
self.backgroundColor = theme.colors.background
- playButton.backgroundColor = theme.colors.background
+ playButton.backgroundColor = theme.roomCellIncomingBubbleBackgroundColor
playButton.tintColor = theme.colors.secondaryContent
let backgroundViewColor = self.customBackgroundViewColor ?? theme.colors.quinaryContent
@@ -145,7 +145,8 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable {
backgroundView.backgroundColor = backgroundViewColor
_waveformView.primaryLineColor = theme.colors.quarterlyContent
_waveformView.secondaryLineColor = theme.colors.secondaryContent
- elapsedTimeLabel.textColor = theme.colors.tertiaryContent
+ elapsedTimeLabel.textColor = theme.colors.secondaryContent
+ elapsedTimeLabel.font = theme.fonts.body
}
func getRequiredNumberOfSamples() -> Int {
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib
index 943832e99..7b3011954 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -22,7 +22,7 @@
-
+
@@ -48,7 +48,7 @@
-
+
@@ -66,8 +66,8 @@
-
-
+
+
diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m
index d06b4a577..991f4544a 100644
--- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m
+++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m
@@ -24,6 +24,7 @@
#import "GeneratedInterface-Swift.h"
+@import DesignKit;
enum
{
diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m
index 6d5c7c10b..297e0b6eb 100644
--- a/Riot/Modules/Settings/Security/SecurityViewController.m
+++ b/Riot/Modules/Settings/Security/SecurityViewController.m
@@ -26,6 +26,8 @@
#import "GeneratedInterface-Swift.h"
+@import DesignKit;
+
// Dev flag to have more options
//#define CROSS_SIGNING_AND_BACKUP_DEV
@@ -1443,7 +1445,7 @@ TableViewSectionsDelegate>
currentAlert = exportView.alertController;
// Use a temporary file for the export
- keyExportsFile = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"riot-keys.txt"]];
+ keyExportsFile = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"element-keys.txt"]];
// Make sure the file is empty
[self deleteKeyExportFile];
diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m
index f8e383773..b26daf997 100644
--- a/Riot/Modules/Settings/SettingsViewController.m
+++ b/Riot/Modules/Settings/SettingsViewController.m
@@ -45,6 +45,8 @@
#import "GeneratedInterface-Swift.h"
+@import DesignKit;
+
NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId";
typedef NS_ENUM(NSUInteger, SECTION_TAG)
@@ -103,6 +105,7 @@ typedef NS_ENUM(NSUInteger, NOTIFICATION_SETTINGS)
{
NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0,
NOTIFICATION_SETTINGS_SYSTEM_SETTINGS,
+ NOTIFICATION_SETTINGS_SHOW_IN_APP_INDEX,
NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT,
NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX,
NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX,
@@ -410,6 +413,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
Section *sectionNotificationSettings = [Section sectionWithTag:SECTION_TAG_NOTIFICATIONS];
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX];
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_SYSTEM_SETTINGS];
+ [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_SHOW_IN_APP_INDEX];
if (RiotSettings.shared.settingsScreenShowNotificationDecodedContentOption)
{
[sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT];
@@ -2076,6 +2080,18 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
[cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme];
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
}
+ else if (row == NOTIFICATION_SETTINGS_SHOW_IN_APP_INDEX)
+ {
+ MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
+
+ labelAndSwitchCell.mxkLabel.text = VectorL10n.settingsEnableInappNotifications;
+ labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.showInAppNotifications;
+ labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
+ labelAndSwitchCell.mxkSwitch.enabled = account.pushNotificationServiceIsActive;
+ [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleShowInAppNotifications:) forControlEvents:UIControlEventTouchUpInside];
+
+ cell = labelAndSwitchCell;
+ }
else if (row == NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT)
{
MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
@@ -3165,6 +3181,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
}
}
+- (void)toggleShowInAppNotifications:(UISwitch *)sender
+{
+ RiotSettings.shared.showInAppNotifications = sender.isOn;
+}
+
- (void)openSystemSettingsApp
{
NSURL *settingsAppURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift
index cf0e09844..eff81d0e5 100644
--- a/Riot/Modules/SplitView/SplitViewCoordinator.swift
+++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift
@@ -156,6 +156,18 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType {
self.tabBarCoordinator?.popToHome(animated: animated, completion: completion)
}
+ func showErroIndicator(with error: Error) {
+ tabBarCoordinator?.showErroIndicator(with: error)
+ }
+
+ func hideAppStateIndicator() {
+ tabBarCoordinator?.hideAppStateIndicator()
+ }
+
+ func showAppStateIndicator(with text: String, icon: UIImage?) {
+ tabBarCoordinator?.showAppStateIndicator(with: text, icon: icon)
+ }
+
// MARK: - Private methods
private func createPlaceholderDetailsViewController() -> UIViewController {
diff --git a/Riot/Modules/SplitView/SplitViewCoordinatorType.swift b/Riot/Modules/SplitView/SplitViewCoordinatorType.swift
index 4fd2cfef6..cfd04ed3e 100644
--- a/Riot/Modules/SplitView/SplitViewCoordinatorType.swift
+++ b/Riot/Modules/SplitView/SplitViewCoordinatorType.swift
@@ -36,4 +36,13 @@ protocol SplitViewCoordinatorType: Coordinator, Presentable {
// TODO: Do not expose publicly this method
/// Remove detail screens and display placeholder if needed
func resetDetails(animated: Bool)
+
+ /// Displays an error using a `UserIndicator`. The messages is dimissed automatically.
+ func showErroIndicator(with error: Error)
+
+ /// Displays an message related to the application state using a `UserIndicator`. The message must be dimissed by calling the method `hideAppStateIndicator()`
+ func showAppStateIndicator(with text: String, icon: UIImage?)
+
+ /// Hide the message related to the application state currently displayed.
+ func hideAppStateIndicator()
}
diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift
index a35e0429f..73f61b231 100644
--- a/Riot/Modules/TabBar/TabBarCoordinator.swift
+++ b/Riot/Modules/TabBar/TabBarCoordinator.swift
@@ -29,7 +29,10 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
private let parameters: TabBarCoordinatorParameters
private let activityIndicatorPresenter: ActivityIndicatorPresenterType
private let indicatorPresenter: UserIndicatorTypePresenterProtocol
-
+ private let userIndicatorStore: UserIndicatorStore
+ private var appStateIndicatorCancel: UserIndicatorCancel?
+ private var appSateIndicator: UserIndicator?
+
// Indicate if the Coordinator has started once
private var hasStartedOnce: Bool {
return self.masterTabBarController != nil
@@ -84,6 +87,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
self.masterNavigationController = masterNavigationController
self.activityIndicatorPresenter = ActivityIndicatorPresenter()
self.indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: masterNavigationController)
+ self.userIndicatorStore = UserIndicatorStore(presenter: indicatorPresenter)
}
// MARK: - Public methods
@@ -190,6 +194,37 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
}
}
+ func showErroIndicator(with error: Error) {
+ let error = error as NSError
+
+ // Ignore fake error, or connection cancellation error
+ guard error.domain != NSURLErrorDomain || error.code != NSURLErrorCancelled else {
+ return
+ }
+
+ // Ignore GDPR Consent not given error. Already caught by kMXHTTPClientUserConsentNotGivenErrorNotification observation
+ let mxError = MXError.isMXError(error) ? MXError(nsError: error) : nil
+ guard mxError?.errcode != kMXErrCodeStringConsentNotGiven else {
+ return
+ }
+
+ let msg = error.userInfo[NSLocalizedFailureReasonErrorKey] as? String
+ let localizedDescription = error.userInfo[NSLocalizedDescriptionKey] as? String
+ let title = (error.userInfo[NSLocalizedFailureReasonErrorKey] as? String) ?? (msg ?? (localizedDescription ?? VectorL10n.error))
+
+ indicators.append(self.indicatorPresenter.present(.failure(label: title)))
+ }
+
+ func showAppStateIndicator(with text: String, icon: UIImage?) {
+ hideAppStateIndicator()
+ appSateIndicator = self.indicatorPresenter.present(.custom(label: text, icon: icon))
+ }
+
+ func hideAppStateIndicator() {
+ appSateIndicator?.cancel()
+ appSateIndicator = nil
+ }
+
// MARK: - SplitViewMasterPresentable
var selectedNavigationRouter: NavigationRouterType? {
diff --git a/Riot/Modules/TabBar/TabBarCoordinatorType.swift b/Riot/Modules/TabBar/TabBarCoordinatorType.swift
index 8d916606a..2d6d290b0 100644
--- a/Riot/Modules/TabBar/TabBarCoordinatorType.swift
+++ b/Riot/Modules/TabBar/TabBarCoordinatorType.swift
@@ -37,4 +37,13 @@ protocol TabBarCoordinatorType: Coordinator, SplitViewMasterPresentable {
// TODO: Remove this method, this implementation detail should not be exposed
// Release the current selected item (room/contact/group...).
func releaseSelectedItems()
+
+ /// Displays an error using a `UserIndicator`. The messages is dimissed automatically.
+ func showErroIndicator(with error: Error)
+
+ /// Displays an message related to the application state using a `UserIndicator`. The message must be dimissed by calling the method `hideAppStateIndicator()`
+ func showAppStateIndicator(with text: String, icon: UIImage?)
+
+ /// Hide the message related to the application state currently displayed.
+ func hideAppStateIndicator()
}
diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist
index 668798a9a..4eaf0aa24 100644
--- a/Riot/SupportingFiles/Info.plist
+++ b/Riot/SupportingFiles/Info.plist
@@ -139,8 +139,6 @@
im.vector.app.pills
- UserDefaults
- ${PRODUCT_NAME}-Defaults
applicationGroupIdentifier
$(APPLICATION_GROUP_IDENTIFIER)
baseBundleIdentifier
diff --git a/Riot/target.yml b/Riot/target.yml
index ec016f09a..5a0505ad7 100644
--- a/Riot/target.yml
+++ b/Riot/target.yml
@@ -34,8 +34,8 @@ targets:
- target: RiotShareExtension
- target: SiriIntents
- target: RiotNSE
- - target: DesignKit
- target: CommonKit
+ - package: DesignKit
- package: Mapbox
- package: OrderedCollections
diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml
index bf2a032f5..c50705aa0 100644
--- a/RiotShareExtension/target.yml
+++ b/RiotShareExtension/target.yml
@@ -30,6 +30,8 @@ targets:
RiotShareExtension:
platform: iOS
type: app-extension
+ dependencies:
+ - package: DesignKit
configFiles:
Debug: Debug.xcconfig
diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift
index 02cfc1ba5..ac95c3d69 100644
--- a/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift
+++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift
@@ -38,7 +38,7 @@ enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable
let viewModel: AuthenticationServerSelectionViewModel
switch self {
case .matrix:
- viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "https://matrix.org",
+ viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
hasModalPresentation: true)
case .emptyAddress:
viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "",
@@ -48,7 +48,7 @@ enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable
hasModalPresentation: true)
Task { await viewModel.displayError(.footerMessage(VectorL10n.errorCommonMessage)) }
case .nonModal:
- viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "https://matrix.org",
+ viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "matrix.org",
hasModalPresentation: false)
}
diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift
index 84a39a4f4..4908493f2 100644
--- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift
+++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift
@@ -43,7 +43,7 @@ class AuthenticationServerSelectionUITests: MockScreenTest {
func verifyNormalState() {
let serverTextField = app.textFields.element
- XCTAssertEqual(serverTextField.value as? String, "matrix.org", "The server shown should be matrix.org with the https scheme hidden.")
+ XCTAssertEqual(serverTextField.value as? String, "matrix.org", "The server shown should be matrix.org as passed to the view model init.")
let confirmButton = app.buttons["confirmButton"]
XCTAssertEqual(confirmButton.label, VectorL10n.confirm, "The confirm button should say Confirm when in modal presentation.")
diff --git a/DesignKit/Source/AvatarSize.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarSize.swift
similarity index 95%
rename from DesignKit/Source/AvatarSize.swift
rename to RiotSwiftUI/Modules/Common/Avatar/Model/AvatarSize.swift
index bac46e6f3..09daff1d2 100644
--- a/DesignKit/Source/AvatarSize.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarSize.swift
@@ -17,6 +17,8 @@
import Foundation
import UIKit
+// TODO: Move into element-design-tokens repo.
+
// Figma Avatar Sizes: https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1258%3A19678
public enum AvatarSize: Int {
case xxSmall = 16
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
index 2b7fa9e60..55904b5d1 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
@@ -49,7 +49,7 @@ struct AvatarImage: View {
mxContentUri: mxContentUri,
matrixItemId: matrixItemId,
displayName: displayName,
- colorCount: theme.colors.namesAndAvatars.count,
+ colorCount: theme.colors.contentAndAvatars.count,
avatarSize: size
)
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift
index f119a7e14..7dbc2ba4f 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift
@@ -36,7 +36,7 @@ struct PlaceholderAvatarImage: View {
var body: some View {
ZStack {
- theme.colors.namesAndAvatars[colorIndex]
+ theme.colors.contentAndAvatars[colorIndex]
Text(String(firstCharacter))
.padding(4)
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
index d82e2107f..31e734c58 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
@@ -38,7 +38,7 @@ struct SpaceAvatarImage: View {
.padding(10)
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
.foregroundColor(.white)
- .background(theme.colors.namesAndAvatars[colorIndex])
+ .background(theme.colors.contentAndAvatars[colorIndex])
.clipShape(RoundedRectangle(cornerRadius: 8))
// Make the text resizable (i.e. Make it large and then allow it to scale down)
.font(.system(size: 200))
@@ -55,7 +55,7 @@ struct SpaceAvatarImage: View {
mxContentUri: mxContentUri,
matrixItemId: matrixItemId,
displayName: value,
- colorCount: theme.colors.namesAndAvatars.count,
+ colorCount: theme.colors.contentAndAvatars.count,
avatarSize: size
)
})
@@ -65,7 +65,7 @@ struct SpaceAvatarImage: View {
mxContentUri: mxContentUri,
matrixItemId: matrixItemId,
displayName: displayName,
- colorCount: theme.colors.namesAndAvatars.count,
+ colorCount: theme.colors.contentAndAvatars.count,
avatarSize: size
)
}
diff --git a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift
index 67ef9bc7c..daf298461 100644
--- a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift
+++ b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift
@@ -31,7 +31,7 @@ class EffectsScene: SCNScene {
static func confetti(with theme: ThemeSwiftUI) -> EffectsScene? {
guard let scene = EffectsScene(named: Constants.confettiSceneName) else { return nil }
- let colors: [[Float]] = theme.colors.namesAndAvatars.compactMap { $0.floatComponents }
+ let colors: [[Float]] = theme.colors.contentAndAvatars.compactMap { $0.floatComponents }
if let particles = scene.rootNode.childNode(withName: Constants.particlesNodeName, recursively: false)?.particleSystems?.first {
// The particles need a non-zero color variation for the handler to affect the color
@@ -65,9 +65,12 @@ fileprivate extension Color {
///
/// SceneKit works in a colorspace with a linear gamma, which is why this conversion is necessary.
var floatComponents: [Float]? {
+ // Get the CGColor from a UIColor as it is nil on Color when loaded from an asset catalog.
+ let cgColor = UIColor(self).cgColor
+
guard
let colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
- let linearColor = cgColor?.converted(to: colorSpace, intent: .defaultIntent, options: nil),
+ let linearColor = cgColor.converted(to: colorSpace, intent: .defaultIntent, options: nil),
let components = linearColor.components
else { return nil }
diff --git a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift
index 2422a2ef5..4ab2c5746 100644
--- a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift
+++ b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift
@@ -59,3 +59,9 @@ struct EffectsView: UIViewRepresentable {
}
}
}
+
+struct EffectsView_Previews: PreviewProvider {
+ static var previews: some View {
+ EffectsView(effect: .confetti)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
index a55633467..c53ee2bdc 100644
--- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
+++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
@@ -19,6 +19,7 @@ import Foundation
/// The static list of mocked screens in RiotSwiftUI
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
+ MockLiveLocationLabPromotionScreenState.self,
MockLiveLocationSharingViewerScreenState.self,
MockAuthenticationLoginScreenState.self,
MockAuthenticationReCaptchaScreenState.self,
diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift
index f5a15424f..7e8fe5308 100644
--- a/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift
+++ b/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift
@@ -14,10 +14,33 @@
// limitations under the License.
//
-import Foundation
+import SwiftUI
import DesignKit
+import DesignTokens
protocol ThemeSwiftUI: ThemeSwiftUIType {
var identifier: ThemeIdentifier { get }
var isDark: Bool { get }
}
+
+/// Theme v2 for SwiftUI.
+@available(iOS 14.0, *)
+public protocol ThemeSwiftUIType {
+
+ /// Colors object
+ var colors: ElementColors { get }
+
+ /// Fonts object
+ var fonts: ElementFonts { get }
+
+ /// may contain more design components in future, like icons, audio files etc.
+}
+
+// MARK: - Legacy Colors
+
+public extension ElementColors {
+ var legacyTile: Color {
+ let dynamicColor = UIColor { $0.userInterfaceStyle == .light ? .elementLight.tile : .elementDark.tile }
+ return Color(dynamicColor)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift
index ad1eeb222..6c3e3c2e2 100644
--- a/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift
+++ b/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift
@@ -23,7 +23,7 @@ extension ThemeSwiftUI {
/// - Parameter userId: The user id used to hash.
/// - Returns: The SwiftUI color for the associated userId.
func userColor(for userId: String) -> Color {
- let senderNameColorIndex = Int(userId.vc_hashCode % Int32(colors.namesAndAvatars.count))
- return colors.namesAndAvatars[senderNameColorIndex]
+ let senderNameColorIndex = Int(userId.vc_hashCode % Int32(colors.contentAndAvatars.count))
+ return colors.contentAndAvatars[senderNameColorIndex]
}
}
diff --git a/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift
index 0e9250070..a572a4694 100644
--- a/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift
+++ b/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift
@@ -14,12 +14,12 @@
// limitations under the License.
//
-import Foundation
+import SwiftUI
import DesignKit
struct DarkThemeSwiftUI: ThemeSwiftUI {
var identifier: ThemeIdentifier = .dark
let isDark: Bool = true
- var colors: ColorSwiftUI = DarkColors.swiftUI
- var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts())
+ var colors = Color.element
+ var fonts = Font.element
}
diff --git a/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift
index 85ba4d810..bfc2e87c0 100644
--- a/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift
+++ b/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift
@@ -14,12 +14,12 @@
// limitations under the License.
//
-import Foundation
+import SwiftUI
import DesignKit
struct DefaultThemeSwiftUI: ThemeSwiftUI {
var identifier: ThemeIdentifier = .light
let isDark: Bool = false
- var colors: ColorSwiftUI = LightColors.swiftUI
- var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts())
+ var colors = Color.element
+ var fonts = Font.element
}
diff --git a/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift
index fe75aa300..2615efa47 100644
--- a/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift
+++ b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift
@@ -50,7 +50,7 @@ struct BorderedInputFieldStyle: TextFieldStyle {
if (theme.identifier == ThemeIdentifier.dark) {
return (isEnabled ? theme.colors.primaryContent : theme.colors.tertiaryContent)
} else {
- return (isEnabled ? theme.colors.primaryContent : theme.colors.quarterlyContent)
+ return (isEnabled ? theme.colors.primaryContent : theme.colors.quaternaryContent)
}
}
diff --git a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift
index 7eb67d39c..6ab0832ac 100644
--- a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift
+++ b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift
@@ -47,7 +47,7 @@ struct ClearViewModifier: ViewModifier {
}) {
Image(systemName: "xmark.circle.fill")
.renderingMode(.template)
- .foregroundColor(theme.colors.quarterlyContent)
+ .foregroundColor(theme.colors.quaternaryContent)
}
.padding(.top, alignment == .top ? 8 : 0)
.padding(.bottom, alignment == .bottom ? 8 : 0)
diff --git a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift
index 5e20f11b0..a03c4b21b 100644
--- a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift
+++ b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift
@@ -56,7 +56,7 @@ struct MultilineTextField: View {
return theme.colors.accent
}
- return theme.colors.quarterlyContent
+ return theme.colors.quaternaryContent
}
private var borderWidth: CGFloat {
@@ -75,7 +75,7 @@ struct MultilineTextField: View {
.overlay(rect.stroke(borderColor, lineWidth: borderWidth))
.introspectTextView { textView in
textView.textColor = UIColor(textColor)
- textView.font = theme.fonts.uiFonts.callout
+ textView.font = .element.callout
}
}
diff --git a/RiotSwiftUI/Modules/Common/Util/OptionButton.swift b/RiotSwiftUI/Modules/Common/Util/OptionButton.swift
index 17e54bbda..428ecf09c 100644
--- a/RiotSwiftUI/Modules/Common/Util/OptionButton.swift
+++ b/RiotSwiftUI/Modules/Common/Util/OptionButton.swift
@@ -55,7 +55,7 @@ struct OptionButton: View {
}
}
Spacer()
- Image(systemName: "chevron.right").font(.system(size: 16, weight: .regular)).foregroundColor(theme.colors.quarterlyContent)
+ Image(systemName: "chevron.right").font(.system(size: 16, weight: .regular)).foregroundColor(theme.colors.quaternaryContent)
}
.padding(EdgeInsets(top: 15, leading: 16, bottom: 15, trailing: 16))
.background(theme.colors.quinaryContent)
diff --git a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift
index 4edaa2e5c..3901ea65a 100644
--- a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift
+++ b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift
@@ -38,7 +38,7 @@ struct SearchBar: View {
}
.padding(8)
.padding(.horizontal, 25)
- .background(theme.colors.navigation)
+ .background(theme.colors.system)
.cornerRadius(8)
.padding(.leading)
.padding(.trailing, isEditing ? 8 : 16)
@@ -46,7 +46,7 @@ struct SearchBar: View {
HStack {
Image(systemName: "magnifyingglass")
.renderingMode(.template)
- .foregroundColor(theme.colors.quarterlyContent)
+ .foregroundColor(theme.colors.quaternaryContent)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if isEditing && !text.isEmpty {
@@ -55,7 +55,7 @@ struct SearchBar: View {
}) {
Image(systemName: "multiply.circle.fill")
.renderingMode(.template)
- .foregroundColor(theme.colors.quarterlyContent)
+ .foregroundColor(theme.colors.quaternaryContent)
}
}
}
diff --git a/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift
index 8f0eb6aac..a20eba58e 100644
--- a/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift
+++ b/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift
@@ -68,7 +68,7 @@ struct SecondaryActionButtonStyle_Previews: PreviewProvider {
Text("Custom")
.foregroundColor(theme.colors.secondaryContent)
}
- .buttonStyle(SecondaryActionButtonStyle(customColor: theme.colors.quarterlyContent))
+ .buttonStyle(SecondaryActionButtonStyle(customColor: theme.colors.quaternaryContent))
}
.padding()
}
diff --git a/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift b/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift
index 60f7315bb..4abada5e5 100644
--- a/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift
+++ b/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift
@@ -89,7 +89,7 @@ struct WaitOverlay: ViewModifier {
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 8, style: .continuous)
- .fill(theme.colors.navigation.opacity(0.9)))
+ .fill(theme.colors.system.opacity(0.9)))
}
.edgesIgnoringSafeArea(.all)
.transition(.opacity)
diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Coordinator/LiveLocationLabPromotionCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Coordinator/LiveLocationLabPromotionCoordinator.swift
new file mode 100644
index 000000000..fbc5c6c9f
--- /dev/null
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Coordinator/LiveLocationLabPromotionCoordinator.swift
@@ -0,0 +1,76 @@
+//
+// 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
+
+final class LiveLocationLabPromotionCoordinator: NSObject, Coordinator, Presentable {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let liveLocationLabPromotionHostingController: VectorHostingController
+ private var liveLocationLabPromotionViewModel: LiveLocationLabPromotionViewModelProtocol
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+
+ /// Closure called when coordinator completes. Indicates true if the lab flag has been enabled.
+ var completion: ((Bool) -> Void)?
+
+ // MARK: - Setup
+
+ override init() {
+ let viewModel = LiveLocationLabPromotionViewModel()
+ let view = LiveLocationLabPromotionView(viewModel: viewModel.context)
+ liveLocationLabPromotionViewModel = viewModel
+ liveLocationLabPromotionHostingController = VectorHostingController(rootView: view)
+ liveLocationLabPromotionHostingController.bottomSheetPreferences = VectorHostingBottomSheetPreferences()
+
+ super.init()
+ }
+
+ // MARK: - Public
+
+ func start() {
+ MXLog.debug("[LiveLocationLabPromotionCoordinator] did start.")
+
+ self.liveLocationLabPromotionViewModel.completion = { [weak self] enableLiveLocation in
+ guard let self = self else { return }
+
+ RiotSettings.shared.enableLiveLocationSharing = enableLiveLocation
+
+ self.completion?(enableLiveLocation)
+ }
+
+ liveLocationLabPromotionHostingController.presentationController?.delegate = self
+ }
+
+ func toPresentable() -> UIViewController {
+ return self.liveLocationLabPromotionHostingController
+ }
+}
+
+// MARK: - UIAdaptivePresentationControllerDelegate
+
+extension LiveLocationLabPromotionCoordinator: UIAdaptivePresentationControllerDelegate {
+
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ self.completion?(RiotSettings.shared.enableLiveLocationSharing)
+ }
+}
diff --git a/DesignKit/Common.xcconfig b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionModels.swift
similarity index 59%
rename from DesignKit/Common.xcconfig
rename to RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionModels.swift
index eb4b88c16..266b2d01c 100644
--- a/DesignKit/Common.xcconfig
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionModels.swift
@@ -1,5 +1,5 @@
//
-// Copyright 2021 Vector Creations Ltd
+// 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.
@@ -14,15 +14,19 @@
// limitations under the License.
//
-// Configuration settings file format documentation can be found at:
-// https://help.apple.com/xcode/#/dev745c5c974
+import Foundation
-#include "Config/AppIdentifiers.xcconfig"
-#include "Config/AppVersion.xcconfig"
+// MARK: View
-PRODUCT_NAME = DesignKit
-PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).designkit
+struct LiveLocationLabPromotionViewState: BindableState {
-INFOPLIST_FILE = DesignKit/Info.plist
+ var bindings: LiveLocationLabPromotionBindings
+}
-SKIP_INSTALL = YES
+struct LiveLocationLabPromotionBindings {
+ var enableLabFlag: Bool
+}
+
+enum LiveLocationLabPromotionViewAction {
+ case complete
+}
diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModel.swift
new file mode 100644
index 000000000..eb4ea189c
--- /dev/null
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModel.swift
@@ -0,0 +1,48 @@
+//
+// 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
+
+typealias LiveLocationLabPromotionViewModelType = StateStoreViewModel
+
+class LiveLocationLabPromotionViewModel: LiveLocationLabPromotionViewModelType, LiveLocationLabPromotionViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ // MARK: Public
+
+ var completion: ((Bool) -> Void)?
+
+ // MARK: - Setup
+
+ init() {
+ let bindings = LiveLocationLabPromotionBindings(enableLabFlag: false)
+ super.init(initialViewState: LiveLocationLabPromotionViewState(bindings: bindings))
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: LiveLocationLabPromotionViewAction) {
+ switch viewAction {
+ case .complete:
+ completion?(self.state.bindings.enableLabFlag)
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModelProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModelProtocol.swift
new file mode 100644
index 000000000..a8d741751
--- /dev/null
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModelProtocol.swift
@@ -0,0 +1,25 @@
+//
+// 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
+
+protocol LiveLocationLabPromotionViewModelProtocol {
+
+ /// Closure called when screen completes. Indicates true if the lab flag has been enabled.
+ var completion: ((Bool) -> Void)? { get set }
+
+ var context: LiveLocationLabPromotionViewModelType.Context { get }
+}
diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/MockLiveLocationLabPromotionScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/MockLiveLocationLabPromotionScreenState.swift
new file mode 100644
index 000000000..f062bfe5f
--- /dev/null
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/MockLiveLocationLabPromotionScreenState.swift
@@ -0,0 +1,44 @@
+//
+// 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 SwiftUI
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+enum MockLiveLocationLabPromotionScreenState: MockScreenState, CaseIterable {
+ // A case for each state you want to represent
+ // with specific, minimal associated data that will allow you
+ // mock that screen.
+ case labFlagOff
+
+ /// The associated screen
+ var screenType: Any.Type {
+ LiveLocationLabPromotionView.self
+ }
+
+ /// Generate the view struct for the screen state.
+ var screenView: ([Any], AnyView) {
+ let viewModel = LiveLocationLabPromotionViewModel()
+
+ // can simulate service and viewModel actions here if needs be.
+
+ return (
+ [self, viewModel],
+ AnyView(LiveLocationLabPromotionView(viewModel: viewModel.context)
+ ))
+ }
+}
diff --git a/DesignKit/Debug.xcconfig b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift
similarity index 75%
rename from DesignKit/Debug.xcconfig
rename to RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift
index 11a7288a4..20fe9f0a5 100644
--- a/DesignKit/Debug.xcconfig
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift
@@ -1,5 +1,5 @@
-//
-// Copyright 2021 Vector Creations Ltd
+//
+// 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.
@@ -14,7 +14,9 @@
// limitations under the License.
//
-// Configuration settings file format documentation can be found at:
-// https://help.apple.com/xcode/#/dev745c5c974
+import XCTest
+import RiotSwiftUI
-#include "Common.xcconfig"
+class LiveLocationLabPromotionUITests: MockScreenTest {
+ // Nothing to test as the view is completely static
+}
diff --git a/DesignKit/Release.xcconfig b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/Unit/LiveLocationLabPromotionViewModelTests.swift
similarity index 74%
rename from DesignKit/Release.xcconfig
rename to RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/Unit/LiveLocationLabPromotionViewModelTests.swift
index 11a7288a4..284b27694 100644
--- a/DesignKit/Release.xcconfig
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/Unit/LiveLocationLabPromotionViewModelTests.swift
@@ -1,5 +1,5 @@
//
-// Copyright 2021 Vector Creations Ltd
+// 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.
@@ -14,7 +14,10 @@
// limitations under the License.
//
-// Configuration settings file format documentation can be found at:
-// https://help.apple.com/xcode/#/dev745c5c974
+import XCTest
-#include "Common.xcconfig"
+@testable import RiotSwiftUI
+
+class LiveLocationLabPromotionViewModelTests: XCTestCase {
+ // Nothing to test as there is no mutable state
+}
diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/View/LiveLocationLabPromotionView.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/View/LiveLocationLabPromotionView.swift
new file mode 100644
index 000000000..8ca11dd23
--- /dev/null
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/View/LiveLocationLabPromotionView.swift
@@ -0,0 +1,84 @@
+//
+// 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
+
+struct LiveLocationLabPromotionView: View {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme
+
+ @ObservedObject var viewModel: LiveLocationLabPromotionViewModel.Context
+
+ // MARK: - View
+
+ var body: some View {
+ VStack {
+ VStack {
+ Image(uiImage: Asset.Images.locationLiveIcon.image)
+ .resizable()
+ .frame(width: 60, height: 60)
+ .padding(.top, 15)
+
+ Text(VectorL10n.locationSharingLiveLabPromotionTitle)
+ .font(theme.fonts.title2B)
+ .multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.primaryContent)
+ .padding(.top, 15)
+
+ Text(VectorL10n.locationSharingLiveLabPromotionText)
+ .font(theme.fonts.body)
+ .multilineTextAlignment(.center)
+ .foregroundColor(theme.colors.primaryContent)
+ .padding(.top, 1)
+
+ Toggle(isOn: $viewModel.enableLabFlag) {
+
+ Text(VectorL10n.locationSharingLiveLabPromotionActivation)
+ .font(theme.fonts.body)
+ .foregroundColor(theme.colors.primaryContent)
+ }
+ .padding(.top)
+
+ Button {
+ self.viewModel.send(viewAction: .complete)
+ } label: {
+ Text(VectorL10n.ok)
+ .font(theme.fonts.bodySB)
+ }
+ .buttonStyle(PrimaryActionButtonStyle())
+ .padding(.top, 20)
+ }
+ .padding()
+ }
+ .frame(maxHeight: .infinity)
+ .background(theme.colors.background.ignoresSafeArea())
+ .accentColor(theme.colors.accent)
+ }
+}
+
+// MARK: - Previews
+
+@available(iOS 15.0, *)
+struct LiveLocationLabPromotion_Previews: PreviewProvider {
+ static let stateRenderer = MockLiveLocationLabPromotionScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift
index 216a65ea4..5884e363e 100644
--- a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift
@@ -72,7 +72,7 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = OnboardingAvatarViewModel(userId: parameters.userSession.userId,
displayName: parameters.userSession.account.userDisplayName,
- avatarColorCount: DefaultThemeSwiftUI().colors.namesAndAvatars.count)
+ avatarColorCount: DefaultThemeSwiftUI().colors.contentAndAvatars.count)
viewModel.updateAvatarImage(with: parameters.avatar)
let view = OnboardingAvatarScreen(viewModel: viewModel.context)
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
index 7cf96984a..8982961f6 100644
--- a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
@@ -44,7 +44,7 @@ enum MockOnboardingAvatarScreenState: MockScreenState, CaseIterable {
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
- let avatarColorCount = DefaultThemeSwiftUI().colors.namesAndAvatars.count
+ let avatarColorCount = DefaultThemeSwiftUI().colors.contentAndAvatars.count
let viewModel: OnboardingAvatarViewModel
switch self {
case .placeholderAvatar(let userId, let displayName):
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift
index fd0f284d3..0fe73b772 100644
--- a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift
@@ -23,7 +23,7 @@ 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 avatarColorCount = DefaultThemeSwiftUI().colors.contentAndAvatars.count
static let avatarImage = Asset.Images.appSymbol.image
}
diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift
index 63e8507a0..f2306eaf4 100644
--- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift
+++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPageIndicator.swift
@@ -47,7 +47,7 @@ struct OnboardingSplashScreenPageIndicator: View {
ForEach(0..?
+ var showMapCreditsSheet = false
}
enum LiveLocationSharingViewerViewAction {
@@ -63,4 +64,5 @@ enum LiveLocationSharingViewerViewAction {
case stopSharing
case tapListItem(_ userId: String)
case share(_ annotation: UserLocationAnnotation)
+ case mapCreditsDidTap
}
diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift
index 3a365b627..8da097b41 100644
--- a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift
@@ -69,6 +69,8 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType
self.highlighAnnotation(with: userId)
case .share(let userLocationAnnotation):
completion?(.share(userLocationAnnotation.coordinate))
+ case .mapCreditsDidTap:
+ state.bindings.showMapCreditsSheet.toggle()
}
}
diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift
index 472652420..b6af40483 100644
--- a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift
+++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift
@@ -85,7 +85,7 @@ struct LiveLocationListItem: View {
Button(VectorL10n.locationSharingLiveListItemStopSharingAction) {
onStopSharingAction?()
}
- .font(theme.fonts.caption1)
+ .font(theme.fonts.body)
.foregroundColor(theme.colors.alert)
}
}
diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift
index fbce0630a..fed3e4c41 100644
--- a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift
+++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift
@@ -25,9 +25,13 @@ struct LiveLocationSharingViewer: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
+ @Environment(\.openURL) var openURL
+
var isBottomSheetVisible = true
@State private var isBottomSheetExpanded = false
+ var bottomSheetCollapsedHeight: CGFloat = 150.0
+
// MARK: Public
@ObservedObject var viewModel: LiveLocationSharingViewerViewModel.Context
@@ -50,20 +54,29 @@ struct LiveLocationSharingViewer: View {
errorSubject: viewModel.viewState.errorSubject)
VStack(alignment: .center) {
Spacer()
- MapCreditsView()
- .offset(y: -130)
+ MapCreditsView(action: {
+ viewModel.send(viewAction: .mapCreditsDidTap)
+ })
+ .offset(y: -(bottomSheetCollapsedHeight)) // Put the copyright action above the collapsed bottom sheet
+ .padding(.bottom, 10)
}
+ .ignoresSafeArea()
}
.navigationTitle(VectorL10n.locationSharingLiveViewerTitle)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
- Button(VectorL10n.cancel) {
+ Button(VectorL10n.close) {
viewModel.send(viewAction: .done)
}
}
}
.accentColor(theme.colors.accent)
.bottomSheet(sheet, if: isBottomSheetVisible)
+ .actionSheet(isPresented: $viewModel.showMapCreditsSheet) {
+ return MapCreditsActionSheet(openURL: { url in
+ openURL(url)
+ }).sheet
+ }
.alert(item: $viewModel.alertInfo) { info in
info.alert
}
@@ -85,6 +98,7 @@ struct LiveLocationSharingViewer: View {
}
.padding()
}
+ .background(theme.colors.background.ignoresSafeArea())
}
}
@@ -107,7 +121,7 @@ extension LiveLocationSharingViewer {
var sheet: some BottomSheetView {
BottomSheet(
isExpanded: $isBottomSheetExpanded,
- minHeight: .points(150),
+ minHeight: .points(bottomSheetCollapsedHeight),
maxHeight: .available,
style: sheetStyle) {
userLocationList
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift
index 63158ebf8..f396c1d88 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift
@@ -76,12 +76,13 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
init(parameters: LocationSharingCoordinatorParameters) {
self.parameters = parameters
-
- let locationSharingService = LocationSharingService(userLocationService: parameters.roomDataSource.mxSession.userLocationService)
+ let locationSharingService = LocationSharingService(session: parameters.roomDataSource.mxSession)
let viewModel = LocationSharingViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL,
avatarData: parameters.avatarData,
- isLiveLocationSharingEnabled: RiotSettings.shared.enableLiveLocationSharing, service: locationSharingService)
+ isLiveLocationSharingEnabled: true,
+ service: locationSharingService)
+
let view = LocationSharingView(context: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
@@ -101,6 +102,8 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
self.shareStaticLocation(latitude: latitude, longitude: longitude, coordinateType: coordinateType)
case .shareLiveLocation(let timeout):
self.startLiveLocationSharing(with: timeout)
+ case .showLabFlagPromotionIfNeeded(let completion):
+ self.showLabFlagPromotionIfNeeded(completion: completion)
}
}
}
@@ -160,6 +163,38 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
}
}
+ private func showLabFlagPromotionIfNeeded(completion: @escaping ((Bool) -> Void)) {
+ guard RiotSettings.shared.enableLiveLocationSharing == false else {
+ // Live location sharing lab flag is already enabled, do not present lab flag promotion screen
+ completion(true)
+ return
+ }
+
+ self.showLabFlagPromotion(completion: completion)
+ }
+
+ private func showLabFlagPromotion(completion: @escaping ((Bool) -> Void)) {
+
+ // TODO: Use a NavigationRouter instead of using NavigationView inside LocationSharingView
+ // In order to use `NavigationRouter.present`
+
+ let coordinator = LiveLocationLabPromotionCoordinator()
+ coordinator.start()
+
+ coordinator.completion = { [weak self, weak coordinator] enableLiveLocation in
+ guard let self = self, let coordinator = coordinator else { return }
+ completion(enableLiveLocation)
+
+ coordinator.toPresentable().dismiss(animated: true) {
+ self.remove(childCoordinator: coordinator)
+ }
+ }
+
+ self.locationSharingHostingController.present(coordinator.toPresentable(), animated: true)
+
+ self.add(childCoordinator: coordinator)
+ }
+
// MARK: - Presentable
func toPresentable() -> UIViewController {
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift
index 22720eeff..46d694fcf 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift
@@ -40,12 +40,14 @@ enum LocationSharingViewAction {
case startLiveSharing
case shareLiveLocation(timeout: LiveLocationSharingTimeout)
case userDidPan
+ case mapCreditsDidTap
}
enum LocationSharingViewModelResult {
case cancel
case share(latitude: Double, longitude: Double, coordinateType: LocationSharingCoordinateType)
case shareLiveLocation(timeout: TimeInterval)
+ case showLabFlagPromotionIfNeeded(_ completion: ((Bool) -> Void))
}
enum LocationSharingViewError {
@@ -94,6 +96,7 @@ struct LocationSharingViewStateBindings {
var userLocation: CLLocationCoordinate2D?
var pinLocation: CLLocationCoordinate2D?
var showingTimerSelector = false
+ var showMapCreditsSheet = false
}
enum LocationSharingAlertType {
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift
index f7d1de9d3..1fc7242c3 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift
@@ -86,6 +86,8 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
case .userDidPan:
state.showsUserLocation = false
state.isPinDropSharing = true
+ case .mapCreditsDidTap:
+ state.bindings.showMapCreditsSheet.toggle()
}
}
@@ -138,7 +140,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
}
}
- private func startLiveLocationSharing() {
+ private func checkLocationAuthorizationAndPresentTimerSelector() {
self.locationSharingService.requestAuthorization { [weak self] authorizationStatus in
@@ -158,11 +160,24 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
self.state.bindings.alertInfo = AlertInfo(id: .userLocatingError,
title: VectorL10n.locationSharingAllowBackgroundLocationTitle,
message: VectorL10n.locationSharingAllowBackgroundLocationMessage,
- primaryButton: (VectorL10n.locationSharingAllowBackgroundLocationCancelAction, { [weak self] in self?.state.bindings.showingTimerSelector = true }),
+ primaryButton: (VectorL10n.locationSharingAllowBackgroundLocationCancelAction, {}),
secondaryButton: (VectorL10n.locationSharingAllowBackgroundLocationValidateAction, { UIApplication.shared.vc_openSettings() }))
case .authorizedAlways:
self.state.bindings.showingTimerSelector = true
}
}
}
+
+ private func startLiveLocationSharing() {
+
+ guard let completion = completion else {
+ return
+ }
+
+ completion(.showLabFlagPromotionIfNeeded({ liveLocationEnabled in
+ if liveLocationEnabled {
+ self.checkLocationAuthorizationAndPresentTimerSelector()
+ }
+ }))
+ }
}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/MapCreditsActionSheet.swift b/RiotSwiftUI/Modules/Room/LocationSharing/MapCreditsActionSheet.swift
new file mode 100644
index 000000000..3f08da5ac
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/MapCreditsActionSheet.swift
@@ -0,0 +1,37 @@
+//
+// 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
+
+struct MapCreditsActionSheet {
+
+ // Open URL action
+ let openURL: (URL) -> Void
+
+ // Map credits action sheet
+ var sheet: ActionSheet {
+ ActionSheet(title: Text(VectorL10n.locationSharingMapCreditsTitle),
+ buttons: [
+ .default(Text("© MapTiler")) {
+ openURL(URL(string: "https://www.maptiler.com/copyright/")!)
+ },
+ .default(Text("© OpenStreetMap")) {
+ openURL(URL(string: "https://www.openstreetmap.org/copyright")!)
+ },
+ .cancel()
+ ])
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Service/MatrixSDK/LocationSharingService.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Service/MatrixSDK/LocationSharingService.swift
index 6d40d2ed9..d7692c29e 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/Service/MatrixSDK/LocationSharingService.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Service/MatrixSDK/LocationSharingService.swift
@@ -24,20 +24,26 @@ class LocationSharingService: LocationSharingServiceProtocol {
// MARK: Private
- private let userLocationService: UserLocationServiceProtocol?
+ private let session: MXSession
+
+ private var userLocationService: UserLocationServiceProtocol? {
+ return self.session.userLocationService
+ }
// MARK: Public
// MARK: - Setup
- init(userLocationService: UserLocationServiceProtocol?) {
- self.userLocationService = userLocationService
+ init(session: MXSession) {
+ self.session = session
}
// MARK: - Public
func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) {
guard let userLocationService = self.userLocationService else {
+
+ MXLog.error("[LocationSharingService] No userLocationService found for the current session")
handler(LocationAuthorizationStatus.unknown)
return
}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift
index 020021026..a7871fa93 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift
@@ -50,6 +50,8 @@ class LocationSharingViewModelTests: XCTestCase {
expectation.fulfill()
case .shareLiveLocation:
XCTFail()
+ case .showLabFlagPromotionIfNeeded:
+ XCTFail()
}
}
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift
index 89c86d3ac..c5e3c4b29 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift
@@ -25,6 +25,8 @@ struct LocationSharingView: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
+ @Environment(\.openURL) var openURL
+
// MARK: Public
@ObservedObject var context: LocationSharingViewModel.Context
@@ -34,12 +36,21 @@ struct LocationSharingView: View {
ZStack(alignment: .bottom) {
mapView
VStack(spacing: 0) {
- MapCreditsView()
+ MapCreditsView(action: {
+ context.send(viewAction: .mapCreditsDidTap)
+ })
+ .padding(.bottom, 10.0)
+ .actionSheet(isPresented: $context.showMapCreditsSheet) {
+ return MapCreditsActionSheet(openURL: { url in
+ openURL(url)
+ }).sheet
+ }
buttonsView
.background(theme.colors.background)
.clipShape(RoundedCornerShape(radius: 8, corners: [.topLeft, .topRight]))
}
}
+ .background(theme.colors.background.ignoresSafeArea())
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(VectorL10n.cancel, action: {
diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/MapCreditsView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/MapCreditsView.swift
index da8509918..3edb2fbfc 100644
--- a/RiotSwiftUI/Modules/Room/LocationSharing/View/MapCreditsView.swift
+++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/MapCreditsView.swift
@@ -26,12 +26,20 @@ struct MapCreditsView: View {
// MARK: Public
+ var action: (() -> Void)?
+
var body: some View {
HStack {
- Link("© MapTiler", destination: URL(string: "https://www.maptiler.com/copyright/")!)
- Link("© OpenStreetMap contributors", destination: URL(string: "https://www.openstreetmap.org/copyright")!)
+ Spacer()
+ Button {
+ action?()
+ } label: {
+ Text(VectorL10n.locationSharingMapCreditsTitle)
+ .font(theme.fonts.footnote)
+ .foregroundColor(theme.colors.accent)
+ }
+ .padding(.horizontal)
}
- .font(theme.fonts.caption1)
}
}
diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift
index a38cd0efe..899025c92 100644
--- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift
+++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift
@@ -45,7 +45,7 @@ struct RoomAccessTypeChooserRow: View {
Spacer()
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.renderingMode(.template)
- .foregroundColor(isSelected ? theme.colors.accent : theme.colors.quarterlyContent)
+ .foregroundColor(isSelected ? theme.colors.accent : theme.colors.quaternaryContent)
}
if let badgeText = badgeText {
Text(badgeText)
diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift
index 3b52c9e41..625500bd4 100644
--- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift
+++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift
@@ -37,7 +37,7 @@ struct RoomRestrictedAccessSpaceChooserSelector: View {
Button(VectorL10n.cancel) {
viewModel.send(viewAction: .cancel)
}
- .foregroundColor(viewModel.viewState.loading ? theme.colors.quarterlyContent : theme.colors.accent)
+ .foregroundColor(viewModel.viewState.loading ? theme.colors.quaternaryContent : theme.colors.accent)
.opacity(viewModel.viewState.loading ? 0.7 : 1)
.disabled(viewModel.viewState.loading)
}
@@ -45,7 +45,7 @@ struct RoomRestrictedAccessSpaceChooserSelector: View {
Button(VectorL10n.done) {
viewModel.send(viewAction: .done)
}
- .foregroundColor(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? theme.colors.quarterlyContent : theme.colors.accent)
+ .foregroundColor(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? theme.colors.quaternaryContent : theme.colors.accent)
.opacity(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading ? 0.7 : 1)
.disabled(viewModel.viewState.selectedItemIds.isEmpty || viewModel.viewState.loading)
}
diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
index aac912d9c..2ac5741c0 100644
--- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
@@ -86,7 +86,8 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
}
func toPresentable() -> UIViewController {
- return VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context))
+ return VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context),
+ forceZeroSafeAreaInsets: true)
}
func canEndPoll() -> Bool {
diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift
index 5a3498aa9..a7ac3d534 100644
--- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift
+++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift
@@ -89,10 +89,10 @@ struct TimelinePollAnswerOptionButton: View {
var progressViewAccentColor: Color {
guard !poll.closed else {
- return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent)
+ return (answerOption.winner ? theme.colors.accent : theme.colors.quaternaryContent)
}
- return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent
+ return answerOption.selected ? theme.colors.accent : theme.colors.quaternaryContent
}
}
diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift
index 5bdc3fddc..d534eac16 100644
--- a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift
+++ b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift
@@ -47,15 +47,15 @@ class ChangePasswordUITests: MockScreenTest {
let oldPasswordTextField = app.secureTextFields["oldPasswordTextField"]
XCTAssertTrue(oldPasswordTextField.exists, "The text field should be shown.")
- XCTAssertEqual(oldPasswordTextField.label, "old password", "The text field should be showing the placeholder before text is input.")
+ XCTAssertEqual(oldPasswordTextField.label, "Old password", "The text field should be showing the placeholder before text is input.")
let newPasswordTextField1 = app.secureTextFields["newPasswordTextField1"]
XCTAssertTrue(newPasswordTextField1.exists, "The text field should be shown.")
- XCTAssertEqual(newPasswordTextField1.label, "new password", "The text field should be showing the placeholder before text is input.")
+ XCTAssertEqual(newPasswordTextField1.label, "New password", "The text field should be showing the placeholder before text is input.")
let newPasswordTextField2 = app.secureTextFields["newPasswordTextField2"]
XCTAssertTrue(newPasswordTextField2.exists, "The text field should be shown.")
- XCTAssertEqual(newPasswordTextField2.label, "confirm password", "The text field should be showing the placeholder before text is input.")
+ XCTAssertEqual(newPasswordTextField2.label, "Confirm password", "The text field should be showing the placeholder before text is input.")
let submitButton = app.buttons["submitButton"]
XCTAssertTrue(submitButton.exists, "The submit button should be shown.")
@@ -80,7 +80,7 @@ class ChangePasswordUITests: MockScreenTest {
let newPasswordTextField2 = app.secureTextFields["newPasswordTextField2"]
XCTAssertTrue(newPasswordTextField2.exists, "The text field should be shown.")
- XCTAssertEqual(newPasswordTextField2.label, "confirm password", "The text field should be showing the placeholder before text is input.")
+ XCTAssertEqual(newPasswordTextField2.label, "Confirm password", "The text field should be showing the placeholder before text is input.")
let submitButton = app.buttons["submitButton"]
XCTAssertTrue(submitButton.exists, "The submit button should be shown.")
diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift
index 3b35cd396..beca8dcb5 100644
--- a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift
+++ b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift
@@ -25,7 +25,7 @@ struct FormInputFieldStyle: TextFieldStyle {
private var textColor: Color {
if !isEnabled {
- return theme.colors.quarterlyContent
+ return theme.colors.quaternaryContent
}
return theme.colors.primaryContent
}
diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift
index eeb7743d3..2759038d4 100644
--- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift
+++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift
@@ -144,7 +144,7 @@ struct MatrixItemChooser: View {
}
.padding(.vertical, 4)
.padding(.horizontal)
- .background(theme.colors.tile)
+ .background(theme.colors.legacyTile)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift
index 632403587..a32578c5d 100644
--- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift
+++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift
@@ -52,7 +52,7 @@ struct MatrixItemChooserSectionHeader: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 8)
.padding(.horizontal)
- .background(theme.colors.navigation)
+ .background(theme.colors.system)
.cornerRadius(8)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift
index 42d8f71b3..23f37bb37 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift
@@ -39,7 +39,7 @@ struct SpaceSettings: View {
.padding(.bottom, 32)
}
}
- .background(theme.colors.navigation.ignoresSafeArea())
+ .background(theme.colors.system.ignoresSafeArea())
.waitOverlay(show: viewModel.viewState.isLoading, allowUserInteraction: false)
.ignoresSafeArea(.container, edges: .bottom)
.frame(maxHeight: .infinity)
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift
index 6287ab24d..18bef0d8f 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift
@@ -67,7 +67,7 @@ struct SpaceSettingsOptionListItem: View {
Image(systemName: "chevron.right")
.renderingMode(.template)
.font(.system(size: 16, weight: .regular))
- .foregroundColor(theme.colors.quarterlyContent)
+ .foregroundColor(theme.colors.quaternaryContent)
}
.opacity(isEnabled ? 1 : 0.5)
}
diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml
index 2fc955cb9..3ceb278ab 100644
--- a/RiotSwiftUI/target.yml
+++ b/RiotSwiftUI/target.yml
@@ -30,7 +30,7 @@ targets:
type: application
platform: iOS
dependencies:
- - target: DesignKit
+ - package: DesignKit
- package: Mapbox
sources:
- path: .
diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml
index 6bf4196a4..01060192f 100644
--- a/RiotSwiftUI/targetUITests.yml
+++ b/RiotSwiftUI/targetUITests.yml
@@ -32,6 +32,7 @@ targets:
dependencies:
- target: RiotSwiftUI
+ - package: DesignKit
settings:
base:
diff --git a/RiotTests/MatrixKitTests/MXKEventFormatter+Tests.h b/RiotTests/MatrixKitTests/MXKEventFormatter+Tests.h
index 8aa5a9c1e..e7f3ccb6a 100644
--- a/RiotTests/MatrixKitTests/MXKEventFormatter+Tests.h
+++ b/RiotTests/MatrixKitTests/MXKEventFormatter+Tests.h
@@ -20,5 +20,6 @@
- (NSString*)userDisplayNameFromContentInEvent:(MXEvent*)event withMembershipFilter:(NSString *)filter;
- (NSString*)userAvatarUrlFromContentInEvent:(MXEvent*)event withMembershipFilter:(NSString *)filter;
+- (NSString*)buildHTMLStringForEvent:(MXEvent*)event inReplyToEvent:(MXEvent*)repliedEvent;
@end
diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m
index 089d93810..fbe801665 100644
--- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m
+++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m
@@ -22,7 +22,7 @@
@import DTCoreText;
-@interface MXEventFormatterTests : XCTestCase
+@interface MXKEventFormatterTests : XCTestCase
{
MXKEventFormatter *eventFormatter;
MXEvent *anEvent;
@@ -31,7 +31,7 @@
@end
-@implementation MXEventFormatterTests
+@implementation MXKEventFormatterTests
- (void)setUp
{
diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift b/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift
new file mode 100644
index 000000000..31db30954
--- /dev/null
+++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift
@@ -0,0 +1,76 @@
+//
+// 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 XCTest
+import MatrixSDK
+
+private enum Constants {
+ static let roomId = "someRoomId"
+ static let repliedEventId = "repliedEventId"
+ static let repliedEventBody = "Test message"
+ static let repliedEventEditedBody = "Edited message"
+ static let repliedEventNewContentBody = "New content"
+ static let replyBody = "> <@alice:matrix.org> Test message\n\nReply"
+ static let replyFormattedBodyWithItalic = "In reply to alice
Test message
Reply"
+ static let expectedHTML = "In reply to alice
Test message
Reply"
+ static let expectedEditedHTML = "In reply to alice
Edited message
Reply"
+ static let expectedEditedHTMLWithNewContent = "In reply to alice
New content
Reply"
+ static let expectedEditedHTMLWithParsedItalic = "In reply to alice
New content
Reply"
+}
+
+class MXKEventFormatterTests: XCTestCase {
+ func testBuildHTMLString() {
+ let formatter = MXKEventFormatter()
+ let repliedEvent = MXEvent()
+ let event = MXEvent()
+ func buildHTML() -> String? { return formatter.buildHTMLString(for: event, inReplyTo: repliedEvent) }
+
+ // Initial setup.
+ repliedEvent.sender = "alice"
+ repliedEvent.roomId = Constants.roomId
+ repliedEvent.eventId = Constants.repliedEventId
+ repliedEvent.wireType = kMXEventTypeStringRoomMessage
+ repliedEvent.wireContent = [kMXMessageTypeKey: kMXMessageTypeText,
+ kMXMessageBodyKey: Constants.repliedEventBody]
+ event.sender = "bob"
+ event.wireType = kMXEventTypeStringRoomMessage
+ event.wireContent = [
+ kMXMessageTypeKey: kMXMessageTypeText,
+ kMXMessageBodyKey: Constants.replyBody,
+ kMXEventRelationRelatesToKey: [kMXEventContentRelatesToKeyInReplyTo: ["event_id": Constants.repliedEventId]]
+ ]
+
+ // Default render.
+ XCTAssertEqual(buildHTML(), Constants.expectedHTML)
+
+ // Render after edition.
+ repliedEvent.wireContent[kMXMessageBodyKey] = Constants.repliedEventEditedBody
+ XCTAssertEqual(buildHTML(), Constants.expectedEditedHTML)
+
+ // m.new_content has prioritiy over base content.
+ repliedEvent.wireContent[kMXMessageContentKeyNewContent] = [kMXMessageBodyKey: Constants.repliedEventNewContentBody]
+ XCTAssertEqual(buildHTML(), Constants.expectedEditedHTMLWithNewContent)
+
+ // If reply's formatted_body is available it's used to construct a brand new HTML.
+ event.wireContent["formatted_body"] = Constants.replyFormattedBodyWithItalic
+ XCTAssertEqual(buildHTML(), Constants.expectedEditedHTMLWithParsedItalic)
+
+ // If content from replied event is missing. Reply can't be constructed (client will use fallback).
+ repliedEvent.wireContent[kMXMessageBodyKey] = nil
+ repliedEvent.wireContent[kMXMessageContentKeyNewContent] = nil
+ XCTAssertNil(buildHTML())
+ }
+}
diff --git a/RiotTests/MatrixKitTests/MatrixKitTests-Bridging-Header.h b/RiotTests/MatrixKitTests/MatrixKitTests-Bridging-Header.h
index b3d959c69..648b077d9 100644
--- a/RiotTests/MatrixKitTests/MatrixKitTests-Bridging-Header.h
+++ b/RiotTests/MatrixKitTests/MatrixKitTests-Bridging-Header.h
@@ -3,3 +3,4 @@
//
#import "MXKRoomDataSource+Tests.h"
+#import "MXKEventFormatter+Tests.h"
diff --git a/RiotTests/PillsFormatterTests.swift b/RiotTests/PillsFormatterTests.swift
index 93cfcb76e..3b5d2a83c 100644
--- a/RiotTests/PillsFormatterTests.swift
+++ b/RiotTests/PillsFormatterTests.swift
@@ -21,10 +21,11 @@ import XCTest
private enum Inputs {
static let messageStart = "Hello "
static let aliceDisplayname = "Alice"
+ static let aliceUserId = "@alice:matrix.org"
static let aliceAvatarUrl = "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ"
static let aliceAwayDisplayname = "Alice_away"
static let aliceNewAvatarUrl = "mxc://matrix.org/VyNYAgaFdlLojoOeZETtQ"
- static let aliceMember = FakeMXRoomMember(displayname: aliceDisplayname, avatarUrl: aliceAvatarUrl, userId: "@alice:matrix.org")
+ static let aliceMember = FakeMXRoomMember(displayname: aliceDisplayname, avatarUrl: aliceAvatarUrl, userId: aliceUserId)
static let aliceMemberAway = FakeMXRoomMember(displayname: aliceAwayDisplayname, avatarUrl: aliceNewAvatarUrl, userId: "@alice:matrix.org")
static let bobMember = FakeMXRoomMember(displayname: "Bob", avatarUrl: "", userId: "@bob:matrix.org")
static let alicePermalink = "https://matrix.to/#/@alice:matrix.org"
@@ -77,14 +78,16 @@ class PillsFormatterTests: XCTestCase {
func testPillsToMarkdown() {
let messageWithPills = createMessageWithMentionFromBobToAlice()
- let markdownMessage = PillsFormatter.stringByReplacingPills(in: messageWithPills, asMarkdown: true)
+ let markdownMessage = PillsFormatter.stringByReplacingPills(in: messageWithPills, mode: .markdown)
XCTAssertEqual(markdownMessage, Inputs.messageStart + Inputs.markdownLinkToAlice)
}
func testPillsToRawBody() {
let messageWithPills = createMessageWithMentionFromBobToAlice()
- let rawMessage = PillsFormatter.stringByReplacingPills(in: messageWithPills, asMarkdown: false)
- XCTAssertEqual(rawMessage, Inputs.messageStart + Inputs.aliceDisplayname)
+ let messageWithDisplayname = PillsFormatter.stringByReplacingPills(in: messageWithPills, mode: .displayname)
+ let messageWithUserId = PillsFormatter.stringByReplacingPills(in: messageWithPills, mode: .identifier)
+ XCTAssertEqual(messageWithDisplayname, Inputs.messageStart + Inputs.aliceDisplayname)
+ XCTAssertEqual(messageWithUserId, Inputs.messageStart + Inputs.aliceUserId)
}
}
diff --git a/SiriIntents/ContactResolver/ContactResolver.h b/SiriIntents/ContactResolver/ContactResolver.h
new file mode 100644
index 000000000..e2d091e86
--- /dev/null
+++ b/SiriIntents/ContactResolver/ContactResolver.h
@@ -0,0 +1,26 @@
+//
+// 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
+#import "GeneratedInterface-Swift.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface ContactResolver: NSObject
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/SiriIntents/ContactResolver/ContactResolver.m b/SiriIntents/ContactResolver/ContactResolver.m
new file mode 100644
index 000000000..42d121261
--- /dev/null
+++ b/SiriIntents/ContactResolver/ContactResolver.m
@@ -0,0 +1,177 @@
+//
+// 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 "ContactResolver.h"
+@import Intents;
+#import "MXKAccountManager.h"
+
+@implementation ContactResolver
+
+- (void)resolveContacts:(nullable NSArray *)contacts
+ withCompletion:(void (^)(NSArray * _Nonnull))completion
+{
+ if (contacts.count == 0)
+ {
+ completion(@[[INPersonResolutionResult needsValue]]);
+ return;
+ }
+ else
+ {
+ // We don't iterate over array of contacts from passed intent
+ // since it's hard to imagine scenario with several callee
+ // so we just extract the first one
+ INPerson *callee = contacts.firstObject;
+
+ // If this method is called after selection of the appropriate user, it will hold userId of an user to whom we must call
+ NSString *selectedUserId;
+
+ // Check if the user has selected right room among several direct rooms from previous resolution process run
+ if (callee.customIdentifier.length)
+ {
+ // If callee will have the same name as one of the contact in the system contacts app
+ // Siri will pass us this contact in the intent.contacts array and we must provide the same count of
+ // resolution results as elements count in the intent.contact.
+ // So we just pass the same result at all iterations
+ NSMutableArray *resolutionResults = [NSMutableArray array];
+ for (NSInteger i = 0; i < contacts.count; ++i)
+ [resolutionResults addObject:[INPersonResolutionResult successWithResolvedPerson:callee]];
+ completion(resolutionResults);
+ return;
+ }
+ else
+ {
+ // This resolution process run after selecting appropriate user among suggested user list
+ selectedUserId = callee.personHandle.value;
+ }
+
+ MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
+ if (account)
+ {
+ MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials];
+ [fileStore.roomSummaryStore fetchAllSummaries:^(NSArray> * _Nonnull summaries) {
+
+ // Contains userIds of all users with whom the current user has direct chats
+ // Use set to avoid duplicates
+ NSMutableSet *directUserIds = [NSMutableSet set];
+
+ // Contains room summaries for all direct rooms connected with particular userId
+ NSMutableDictionary> *> *roomSummaries = [NSMutableDictionary dictionary];
+
+ for (id summary in summaries)
+ {
+ // TODO: We also need to check if joined room members count equals 2
+ // It is pointlessly to save rooms with 1 joined member or room with more than 2 joined members
+ if (summary.isDirect)
+ {
+ NSString *directUserId = summary.directUserId;
+
+ // Collect room summaries only for specified user
+ if (selectedUserId && ![directUserId isEqualToString:selectedUserId])
+ continue;
+
+ // Save userId
+ [directUserIds addObject:directUserId];
+
+ // Save associated with diretUserId room summary
+ NSMutableArray> *userRoomSummaries = roomSummaries[directUserId];
+ if (userRoomSummaries)
+ [userRoomSummaries addObject:summary];
+ else
+ roomSummaries[directUserId] = [NSMutableArray arrayWithObject:summary];
+ }
+ }
+
+ [fileStore asyncUsersWithUserIds:directUserIds.allObjects success:^(NSArray * _Nonnull users) {
+
+ // Find users whose display name contains string presented us by Siri
+ NSMutableArray *matchingUsers = [NSMutableArray array];
+ for (MXUser *user in users)
+ {
+ if (!user.displayname)
+ continue;
+
+ if (!NSEqualRanges([callee.displayName rangeOfString:user.displayname options:NSCaseInsensitiveSearch], (NSRange){NSNotFound,0}))
+ {
+ [matchingUsers addObject:user];
+ }
+ }
+
+ NSMutableArray *persons = [NSMutableArray array];
+
+ if (matchingUsers.count == 1)
+ {
+ MXUser *user = matchingUsers.firstObject;
+
+ // Provide to the user a list of direct rooms to choose from
+ NSArray> *summaries = roomSummaries[user.userId];
+ for (id summary in summaries)
+ {
+ INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown];
+
+ // For rooms we try to use room display name
+ NSString *displayName = summary.displayname ? summary.displayname : user.displayname;
+
+ INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle
+ nameComponents:nil
+ displayName:displayName
+ image:nil
+ contactIdentifier:nil
+ customIdentifier:summary.roomId];
+
+ [persons addObject:person];
+ }
+ }
+ else if (matchingUsers.count > 1)
+ {
+ // Provide to the user a list of users to choose from
+ // This is the case when there are several users with the same name
+ for (MXUser *user in matchingUsers)
+ {
+ INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown];
+ INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle
+ nameComponents:nil
+ displayName:user.displayname
+ image:nil
+ contactIdentifier:nil
+ customIdentifier:nil];
+
+ [persons addObject:person];
+ }
+ }
+
+ if (persons.count == 0)
+ {
+ completion(@[[INPersonResolutionResult unsupported]]);
+ }
+ else if (persons.count == 1)
+ {
+ completion(@[[INPersonResolutionResult successWithResolvedPerson:persons.firstObject]]);
+ }
+ else
+ {
+ completion(@[[INPersonResolutionResult disambiguationWithPeopleToDisambiguate:persons]]);
+ }
+ } failure:nil];
+ }];
+ }
+ else
+ {
+ completion(@[[INPersonResolutionResult notRequired]]);
+ }
+ }
+}
+
+@end
diff --git a/SiriIntents/ContactResolver/ContactResolving.swift b/SiriIntents/ContactResolver/ContactResolving.swift
new file mode 100644
index 000000000..542e844bb
--- /dev/null
+++ b/SiriIntents/ContactResolver/ContactResolving.swift
@@ -0,0 +1,22 @@
+//
+// 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 Intents
+
+@objc protocol ContactResolving {
+ func resolveContacts(_ contacts: [INPerson]?,
+ withCompletion completion: @escaping ([INPersonResolutionResult]) -> Void)
+}
diff --git a/SiriIntents/IntentHandler.m b/SiriIntents/IntentHandler.m
index 65681525a..a18c7a843 100644
--- a/SiriIntents/IntentHandler.m
+++ b/SiriIntents/IntentHandler.m
@@ -18,21 +18,23 @@
#import "GeneratedInterface-Swift.h"
#import "MXKAccountManager.h"
+#import "ContactResolver.h"
+#import "StartAudioCallIntentHandler.h"
+#import "StartVideoCallIntentHandler.h"
+#import "SendMessageIntentHandler.h"
#if __has_include()
#define CALL_STACK_JINGLE
#endif
-@interface IntentHandler ()
+@interface IntentHandler ()
// 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;
+@property (nonatomic) id startAudioCallIntentHandler;
+@property (nonatomic) id startVideoCallIntentHandler;
+@property (nonatomic) id sendMessageIntentHandler;
@end
@@ -65,386 +67,26 @@
Analytics *analytics = Analytics.shared;
[MXSDKOptions sharedInstance].analyticsDelegate = analytics;
[analytics startIfEnabled];
+
+ id contactResolver = [[ContactResolver alloc] init];
+ _startAudioCallIntentHandler = [[StartAudioCallIntentHandler alloc] initWithContactResolver:contactResolver];
+ _startVideoCallIntentHandler = [[StartVideoCallIntentHandler alloc] initWithContactResolver:contactResolver];
+ _sendMessageIntentHandler = [[SendMessageIntentHandler alloc] initWithContactResolver:contactResolver];
}
return self;
}
- (id)handlerForIntent:(INIntent *)intent
{
- return self;
-}
-
-#pragma mark - INStartAudioCallIntentHandling
-
-- (void)resolveContactsForStartAudioCall:(INStartAudioCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion
-{
- [self resolveContacts:intent.contacts withCompletion:completion];
-}
-
-- (void)confirmStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion
-{
- INStartAudioCallIntentResponse *response = nil;
-
- MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
- if (account)
- {
-#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined CALL_STACK_JINGLE
- NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartAudioCallIntent class])];
- response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeReady userActivity:userActivity];
-#else
- response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil];
-#endif
- }
- else
- {
- // User hasn't logged in
- response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureAppConfigurationRequired userActivity:nil];
+ if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
+ return self.startAudioCallIntentHandler;
+ } else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
+ return self.startVideoCallIntentHandler;
+ } else if ([intent isKindOfClass:[INSendMessageIntent class]]) {
+ return self.sendMessageIntentHandler;
}
- completion(response);
-}
-
-- (void)handleStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion
-{
- INStartAudioCallIntentResponse *response = nil;
-
- INPerson *person = intent.contacts.firstObject;
- if (person && person.customIdentifier)
- {
- NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartAudioCallIntent.class)];
- userActivity.userInfo = @{ @"roomID" : person.customIdentifier };
-
- response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeContinueInApp
- userActivity:userActivity];
- }
- else
- {
- response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailure userActivity:nil];
- }
-
- completion(response);
-}
-
-#pragma mark - INStartVideoCallIntentHandling
-
-- (void)resolveContactsForStartVideoCall:(INStartVideoCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion
-{
- [self resolveContacts:intent.contacts withCompletion:completion];
-}
-
-- (void)confirmStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion
-{
- INStartVideoCallIntentResponse *response = nil;
-
- MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
- if (account)
- {
-#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined CALL_STACK_JINGLE
- NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartVideoCallIntent class])];
- response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeReady userActivity:userActivity];
-#else
- response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil];
-#endif
- }
- else
- {
- // User hasn't logged in
- response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureRequiringAppLaunch userActivity:nil];
- }
-
- completion(response);
-}
-
-- (void)handleStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion
-{
- INStartVideoCallIntentResponse *response = nil;
-
- INPerson *person = intent.contacts.firstObject;
- if (person && person.customIdentifier)
- {
- NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartVideoCallIntent.class)];
- userActivity.userInfo = @{ @"roomID" : person.customIdentifier };
-
- response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeContinueInApp
- userActivity:userActivity];
- }
- else
- {
- response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailure userActivity:nil];
- }
-
- completion(response);
-}
-
-#pragma mark - INSendMessageIntentHandling
-
-- (void)resolveRecipientsForSendMessage:(INSendMessageIntent *)intent completion:(void (^)(NSArray * _Nonnull))completion
-{
- [self resolveContacts:intent.recipients withCompletion:completion];
-}
-
-- (void)resolveContentForSendMessage:(INSendMessageIntent *)intent withCompletion:(void (^)(INStringResolutionResult * _Nonnull))completion
-{
- NSString *message = intent.content;
- if (message && ![message isEqualToString:@""])
- completion([INStringResolutionResult successWithResolvedString:message]);
- else
- completion([INStringResolutionResult needsValue]);
-}
-
-- (void)confirmSendMessage:(INSendMessageIntent *)intent completion:(void (^)(INSendMessageIntentResponse * _Nonnull))completion
-{
- INSendMessageIntentResponse *response = nil;
-
- MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
- if (account)
- {
- NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INSendMessageIntent class])];
- response = [[INSendMessageIntentResponse alloc] initWithCode:INSendMessageIntentResponseCodeReady userActivity:userActivity];
- }
- else
- {
- // User hasn't logged in
- response = [[INSendMessageIntentResponse alloc] initWithCode:INSendMessageIntentResponseCodeFailureRequiringAppLaunch userActivity:nil];
- }
-
- completion(response);
-}
-
-- (void)handleSendMessage:(INSendMessageIntent *)intent completion:(void (^)(INSendMessageIntentResponse * _Nonnull))completion
-{
- void (^completeWithCode)(INSendMessageIntentResponseCode) = ^(INSendMessageIntentResponseCode code) {
- NSUserActivity *userActivity = nil;
- if (code == INSendMessageIntentResponseCodeSuccess)
- userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INSendMessageIntent class])];
- INSendMessageIntentResponse *response = [[INSendMessageIntentResponse alloc] initWithCode:INSendMessageIntentResponseCodeSuccess
- userActivity:userActivity];
- completion(response);
- };
-
- INPerson *person = intent.recipients.firstObject;
- if (person && person.customIdentifier)
- {
- MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
- MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials];
- [fileStore.roomSummaryStore fetchAllSummaries:^(NSArray> * _Nonnull summaries) {
- NSString *roomID = person.customIdentifier;
-
- BOOL isEncrypted = NO;
- for (id summary in summaries)
- {
- if ([summary.roomId isEqualToString:roomID])
- {
- isEncrypted = summary.isEncrypted;
- break;
- }
- }
-
- if (isEncrypted)
- {
- [MXFileStore setPreloadOptions:0];
-
- MXSession *session = [[MXSession alloc] initWithMatrixRestClient:account.mxRestClient];
- MXWeakify(session);
- [session setStore:fileStore success:^{
- MXStrongifyAndReturnIfNil(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;
-
- 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) {
- completeWithCode(INSendMessageIntentResponseCodeFailure);
- }];
-
- return;
- }
-
- [account.mxRestClient sendTextMessageToRoom:roomID
- threadId:nil
- text:intent.content
- success:^(NSString *eventId) {
- completeWithCode(INSendMessageIntentResponseCodeSuccess);
- }
- failure:^(NSError *error) {
- completeWithCode(INSendMessageIntentResponseCodeFailure);
- }];
-
- }];
- }
- else
- {
- completeWithCode(INSendMessageIntentResponseCodeFailure);
- }
-}
-
-#pragma mark - Private
-
-- (void)resolveContacts:(nullable NSArray *)contacts withCompletion:(void (^)(NSArray * _Nonnull))completion
-{
- if (contacts.count == 0)
- {
- completion(@[[INPersonResolutionResult needsValue]]);
- return;
- }
- else
- {
- // We don't iterate over array of contacts from passed intent
- // since it's hard to imagine scenario with several callee
- // so we just extract the first one
- INPerson *callee = contacts.firstObject;
-
- // If this method is called after selection of the appropriate user, it will hold userId of an user to whom we must call
- NSString *selectedUserId;
-
- // Check if the user has selected right room among several direct rooms from previous resolution process run
- if (callee.customIdentifier.length)
- {
- // If callee will have the same name as one of the contact in the system contacts app
- // Siri will pass us this contact in the intent.contacts array and we must provide the same count of
- // resolution results as elements count in the intent.contact.
- // So we just pass the same result at all iterations
- NSMutableArray *resolutionResults = [NSMutableArray array];
- for (NSInteger i = 0; i < contacts.count; ++i)
- [resolutionResults addObject:[INPersonResolutionResult successWithResolvedPerson:callee]];
- completion(resolutionResults);
- return;
- }
- else
- {
- // This resolution process run after selecting appropriate user among suggested user list
- selectedUserId = callee.personHandle.value;
- }
-
- MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
- if (account)
- {
- MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials];
- [fileStore.roomSummaryStore fetchAllSummaries:^(NSArray> * _Nonnull summaries) {
-
- // Contains userIds of all users with whom the current user has direct chats
- // Use set to avoid duplicates
- NSMutableSet *directUserIds = [NSMutableSet set];
-
- // Contains room summaries for all direct rooms connected with particular userId
- NSMutableDictionary> *> *roomSummaries = [NSMutableDictionary dictionary];
-
- for (id summary in summaries)
- {
- // TODO: We also need to check if joined room members count equals 2
- // It is pointlessly to save rooms with 1 joined member or room with more than 2 joined members
- if (summary.isDirect)
- {
- NSString *directUserId = summary.directUserId;
-
- // Collect room summaries only for specified user
- if (selectedUserId && ![directUserId isEqualToString:selectedUserId])
- continue;
-
- // Save userId
- [directUserIds addObject:directUserId];
-
- // Save associated with diretUserId room summary
- NSMutableArray> *userRoomSummaries = roomSummaries[directUserId];
- if (userRoomSummaries)
- [userRoomSummaries addObject:summary];
- else
- roomSummaries[directUserId] = [NSMutableArray arrayWithObject:summary];
- }
- }
-
- [fileStore asyncUsersWithUserIds:directUserIds.allObjects success:^(NSArray * _Nonnull users) {
-
- // Find users whose display name contains string presented us by Siri
- NSMutableArray *matchingUsers = [NSMutableArray array];
- for (MXUser *user in users)
- {
- if (!user.displayname)
- continue;
-
- if (!NSEqualRanges([callee.displayName rangeOfString:user.displayname options:NSCaseInsensitiveSearch], (NSRange){NSNotFound,0}))
- {
- [matchingUsers addObject:user];
- }
- }
-
- NSMutableArray *persons = [NSMutableArray array];
-
- if (matchingUsers.count == 1)
- {
- MXUser *user = matchingUsers.firstObject;
-
- // Provide to the user a list of direct rooms to choose from
- NSArray> *summaries = roomSummaries[user.userId];
- for (id summary in summaries)
- {
- INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown];
-
- // For rooms we try to use room display name
- NSString *displayName = summary.displayname ? summary.displayname : user.displayname;
-
- INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle
- nameComponents:nil
- displayName:displayName
- image:nil
- contactIdentifier:nil
- customIdentifier:summary.roomId];
-
- [persons addObject:person];
- }
- }
- else if (matchingUsers.count > 1)
- {
- // Provide to the user a list of users to choose from
- // This is the case when there are several users with the same name
- for (MXUser *user in matchingUsers)
- {
- INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown];
- INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle
- nameComponents:nil
- displayName:user.displayname
- image:nil
- contactIdentifier:nil
- customIdentifier:nil];
-
- [persons addObject:person];
- }
- }
-
- if (persons.count == 0)
- {
- completion(@[[INPersonResolutionResult unsupported]]);
- }
- else if (persons.count == 1)
- {
- completion(@[[INPersonResolutionResult successWithResolvedPerson:persons.firstObject]]);
- }
- else
- {
- completion(@[[INPersonResolutionResult disambiguationWithPeopleToDisambiguate:persons]]);
- }
- } failure:nil];
- }];
- }
- else
- {
- completion(@[[INPersonResolutionResult notRequired]]);
- }
- }
+ return nil;
}
@end
diff --git a/DesignKit/DesignKit.h b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.h
similarity index 62%
rename from DesignKit/DesignKit.h
rename to SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.h
index 4ff68e722..37d7fee22 100644
--- a/DesignKit/DesignKit.h
+++ b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.h
@@ -1,5 +1,5 @@
//
-// Copyright 2021 New Vector Ltd
+// 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.
@@ -15,13 +15,15 @@
//
#import
+@import Intents;
+@protocol ContactResolving;
-//! Project version number for DesignKit.
-FOUNDATION_EXPORT double DesignKitVersionNumber;
+NS_ASSUME_NONNULL_BEGIN
-//! Project version string for DesignKit.
-FOUNDATION_EXPORT const unsigned char DesignKitVersionString[];
+@interface SendMessageIntentHandler : NSObject
-// In this header, you should import all the public headers of your framework using statements like #import
+- (instancetype)initWithContactResolver:(id)contactResolver;
+@end
+NS_ASSUME_NONNULL_END
diff --git a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m
new file mode 100644
index 000000000..84d9dee63
--- /dev/null
+++ b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m
@@ -0,0 +1,161 @@
+//
+// 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 "SendMessageIntentHandler.h"
+#import "ContactResolver.h"
+#import "MXKAccountManager.h"
+#import "GeneratedInterface-Swift.h"
+
+@interface SendMessageIntentHandler ()
+
+@property (nonatomic) id contactResolver;
+
+/**
+ 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 SendMessageIntentHandler
+
+- (instancetype)initWithContactResolver:(id)contactResolver
+{
+ if (self = [super init]) {
+ _contactResolver = contactResolver;
+ }
+
+ return self;
+}
+
+#pragma mark - INSendMessageIntentHandling
+
+- (void)resolveRecipientsForSendMessage:(INSendMessageIntent *)intent completion:(void (^)(NSArray * _Nonnull))completion
+{
+ [self.contactResolver resolveContacts:intent.recipients withCompletion:completion];
+}
+
+- (void)resolveContentForSendMessage:(INSendMessageIntent *)intent withCompletion:(void (^)(INStringResolutionResult * _Nonnull))completion
+{
+ NSString *message = intent.content;
+ if (message && ![message isEqualToString:@""])
+ completion([INStringResolutionResult successWithResolvedString:message]);
+ else
+ completion([INStringResolutionResult needsValue]);
+}
+
+- (void)confirmSendMessage:(INSendMessageIntent *)intent completion:(void (^)(INSendMessageIntentResponse * _Nonnull))completion
+{
+ INSendMessageIntentResponse *response = nil;
+
+ MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
+ if (account)
+ {
+ NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INSendMessageIntent class])];
+ response = [[INSendMessageIntentResponse alloc] initWithCode:INSendMessageIntentResponseCodeReady userActivity:userActivity];
+ }
+ else
+ {
+ // User hasn't logged in
+ response = [[INSendMessageIntentResponse alloc] initWithCode:INSendMessageIntentResponseCodeFailureRequiringAppLaunch userActivity:nil];
+ }
+
+ completion(response);
+}
+
+- (void)handleSendMessage:(INSendMessageIntent *)intent completion:(void (^)(INSendMessageIntentResponse * _Nonnull))completion
+{
+ void (^completeWithCode)(INSendMessageIntentResponseCode) = ^(INSendMessageIntentResponseCode code) {
+ NSUserActivity *userActivity = nil;
+ if (code == INSendMessageIntentResponseCodeSuccess)
+ userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INSendMessageIntent class])];
+ INSendMessageIntentResponse *response = [[INSendMessageIntentResponse alloc] initWithCode:INSendMessageIntentResponseCodeSuccess
+ userActivity:userActivity];
+ completion(response);
+ };
+
+ INPerson *person = intent.recipients.firstObject;
+ if (person && person.customIdentifier)
+ {
+ MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
+ MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:account.mxCredentials];
+ [fileStore.roomSummaryStore fetchAllSummaries:^(NSArray> * _Nonnull summaries) {
+ NSString *roomID = person.customIdentifier;
+
+ BOOL isEncrypted = NO;
+ for (id summary in summaries)
+ {
+ if ([summary.roomId isEqualToString:roomID])
+ {
+ isEncrypted = summary.isEncrypted;
+ break;
+ }
+ }
+
+ if (isEncrypted)
+ {
+ [MXFileStore setPreloadOptions:0];
+
+ MXSession *session = [[MXSession alloc] initWithMatrixRestClient:account.mxRestClient];
+ MXWeakify(session);
+ [session setStore:fileStore success:^{
+ MXStrongifyAndReturnIfNil(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;
+
+ 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) {
+ completeWithCode(INSendMessageIntentResponseCodeFailure);
+ }];
+
+ return;
+ }
+
+ [account.mxRestClient sendTextMessageToRoom:roomID
+ threadId:nil
+ text:intent.content
+ success:^(NSString *eventId) {
+ completeWithCode(INSendMessageIntentResponseCodeSuccess);
+ }
+ failure:^(NSError *error) {
+ completeWithCode(INSendMessageIntentResponseCodeFailure);
+ }];
+
+ }];
+ }
+ else
+ {
+ completeWithCode(INSendMessageIntentResponseCodeFailure);
+ }
+}
+
+@end
diff --git a/SiriIntents/IntentHandlers/StartAudioCall/StartAudioCallIntentHandler.h b/SiriIntents/IntentHandlers/StartAudioCall/StartAudioCallIntentHandler.h
new file mode 100644
index 000000000..21f86b2f6
--- /dev/null
+++ b/SiriIntents/IntentHandlers/StartAudioCall/StartAudioCallIntentHandler.h
@@ -0,0 +1,29 @@
+//
+// 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
+@import Intents;
+@protocol ContactResolving;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface StartAudioCallIntentHandler : NSObject
+
+- (instancetype)initWithContactResolver:(id)contactResolver;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/SiriIntents/IntentHandlers/StartAudioCall/StartAudioCallIntentHandler.m b/SiriIntents/IntentHandlers/StartAudioCall/StartAudioCallIntentHandler.m
new file mode 100644
index 000000000..7d328e354
--- /dev/null
+++ b/SiriIntents/IntentHandlers/StartAudioCall/StartAudioCallIntentHandler.m
@@ -0,0 +1,90 @@
+//
+// 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 "StartAudioCallIntentHandler.h"
+#import "MXKAccountManager.h"
+#import "ContactResolver.h"
+#import "GeneratedInterface-Swift.h"
+
+@interface StartAudioCallIntentHandler ()
+
+@property (nonatomic) id contactResolver;
+
+@end
+
+@implementation StartAudioCallIntentHandler
+
+- (instancetype)initWithContactResolver:(id)contactResolver
+{
+ if (self = [super init]) {
+ _contactResolver = contactResolver;
+ }
+
+ return self;
+}
+
+#pragma mark - INStartAudioCallIntentHandling
+
+- (void)resolveContactsForStartAudioCall:(INStartAudioCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion
+{
+ [self.contactResolver resolveContacts:intent.contacts withCompletion:completion];
+}
+
+- (void)confirmStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion
+{
+ INStartAudioCallIntentResponse *response = nil;
+
+ MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
+ if (account)
+ {
+#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined CALL_STACK_JINGLE
+ NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartAudioCallIntent class])];
+ response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeReady userActivity:userActivity];
+#else
+ response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil];
+#endif
+ }
+ else
+ {
+ // User hasn't logged in
+ response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailureAppConfigurationRequired userActivity:nil];
+ }
+
+ completion(response);
+}
+
+- (void)handleStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse * _Nonnull))completion
+{
+ INStartAudioCallIntentResponse *response = nil;
+
+ INPerson *person = intent.contacts.firstObject;
+ if (person && person.customIdentifier)
+ {
+ NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartAudioCallIntent.class)];
+ userActivity.userInfo = @{ @"roomID" : person.customIdentifier };
+
+ response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeContinueInApp
+ userActivity:userActivity];
+ }
+ else
+ {
+ response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeFailure userActivity:nil];
+ }
+
+ completion(response);
+}
+
+@end
diff --git a/SiriIntents/IntentHandlers/StartVideoCall/StartVideoCallIntentHandler.h b/SiriIntents/IntentHandlers/StartVideoCall/StartVideoCallIntentHandler.h
new file mode 100644
index 000000000..496c9fd72
--- /dev/null
+++ b/SiriIntents/IntentHandlers/StartVideoCall/StartVideoCallIntentHandler.h
@@ -0,0 +1,29 @@
+//
+// 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
+@import Intents;
+@protocol ContactResolving;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface StartVideoCallIntentHandler : NSObject
+
+- (instancetype)initWithContactResolver:(id)contactResolver;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/SiriIntents/IntentHandlers/StartVideoCall/StartVideoCallIntentHandler.m b/SiriIntents/IntentHandlers/StartVideoCall/StartVideoCallIntentHandler.m
new file mode 100644
index 000000000..b4fe090d0
--- /dev/null
+++ b/SiriIntents/IntentHandlers/StartVideoCall/StartVideoCallIntentHandler.m
@@ -0,0 +1,90 @@
+//
+// 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 "StartVideoCallIntentHandler.h"
+#import "ContactResolver.h"
+#import "MXKAccountManager.h"
+#import "GeneratedInterface-Swift.h"
+
+@interface StartVideoCallIntentHandler ()
+
+@property (nonatomic) id contactResolver;
+
+@end
+
+@implementation StartVideoCallIntentHandler
+
+- (instancetype)initWithContactResolver:(id)contactResolver
+{
+ if (self = [super init]) {
+ _contactResolver = contactResolver;
+ }
+
+ return self;
+}
+
+#pragma mark - INStartVideoCallIntentHandling
+
+- (void)resolveContactsForStartVideoCall:(INStartVideoCallIntent *)intent withCompletion:(void (^)(NSArray * _Nonnull))completion
+{
+ [self.contactResolver resolveContacts:intent.contacts withCompletion:completion];
+}
+
+- (void)confirmStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion
+{
+ INStartVideoCallIntentResponse *response = nil;
+
+ MXKAccount *account = [MXKAccountManager sharedManager].activeAccounts.firstObject;
+ if (account)
+ {
+#if defined MX_CALL_STACK_OPENWEBRTC || defined MX_CALL_STACK_ENDPOINT || defined CALL_STACK_JINGLE
+ NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartVideoCallIntent class])];
+ response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeReady userActivity:userActivity];
+#else
+ response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureCallingServiceNotAvailable userActivity:nil];
+#endif
+ }
+ else
+ {
+ // User hasn't logged in
+ response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailureRequiringAppLaunch userActivity:nil];
+ }
+
+ completion(response);
+}
+
+- (void)handleStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse * _Nonnull))completion
+{
+ INStartVideoCallIntentResponse *response = nil;
+
+ INPerson *person = intent.contacts.firstObject;
+ if (person && person.customIdentifier)
+ {
+ NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass(INStartVideoCallIntent.class)];
+ userActivity.userInfo = @{ @"roomID" : person.customIdentifier };
+
+ response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeContinueInApp
+ userActivity:userActivity];
+ }
+ else
+ {
+ response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeFailure userActivity:nil];
+ }
+
+ completion(response);
+}
+
+@end
diff --git a/Tools/SwiftGen/swiftgen-config.yml b/Tools/SwiftGen/swiftgen-config.yml
index 957f70fae..7c498ccbc 100755
--- a/Tools/SwiftGen/swiftgen-config.yml
+++ b/Tools/SwiftGen/swiftgen-config.yml
@@ -29,11 +29,4 @@ xcassets:
- Assets/SharedImages.xcassets
outputs:
templatePath: Templates/Assets/swift4-element.stencil
- output: Images.swift
-plist:
- inputs: Assets/Riot-Defaults.plist
- outputs:
- templateName: runtime-swift4
- output: RiotDefaults.swift
- params:
- enumName: RiotDefaults
+ output: Images.swift
\ No newline at end of file
diff --git a/changelog.d/6371.bugfix b/changelog.d/6371.bugfix
deleted file mode 100644
index 7829efb59..000000000
--- a/changelog.d/6371.bugfix
+++ /dev/null
@@ -1 +0,0 @@
-Display fallback when replied event content is partially missing
diff --git a/project.yml b/project.yml
index 84a0cb73f..fb4a0d0ba 100644
--- a/project.yml
+++ b/project.yml
@@ -32,7 +32,6 @@ include:
- path: RiotShareExtension/target.yml
- path: SiriIntents/target.yml
- path: RiotNSE/target.yml
- - path: DesignKit/target.yml
- path: RiotSwiftUI/target.yml
- path: RiotSwiftUI/targetUnitTests.yml
- path: RiotSwiftUI/targetUITests.yml
@@ -40,6 +39,9 @@ include:
- path: CommonKit/targetUnitTests.yml
packages:
+ DesignKit:
+ url: https://github.com/vector-im/element-x-ios
+ exactVersion: 1.0.1-202207011447
Mapbox:
url: https://github.com/maplibre/maplibre-gl-native-distribution
minVersion: 5.12.2