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