Merge branch 'gil/SP1_space_creation' into gil/5231_SP3-1_Update_room_settings_for_Spaces

# Conflicts:
#	Podfile.lock
This commit is contained in:
Gil Eluard
2022-02-21 17:57:58 +01:00
687 changed files with 8777 additions and 3697 deletions
@@ -42,18 +42,13 @@ struct AnalyticsPromptViewState: BindableState {
/// A collection of strings for the UI that need to be created in
/// the coordinator or mocked in the RiotSwiftUI target.
protocol AnalyticsPromptStringsProtocol {
var appDisplayName: String { get }
var point1: NSAttributedString { get }
var point2: NSAttributedString { get }
var termsNewUser: NSAttributedString { get }
var termsUpgrade: NSAttributedString { get }
}
enum AnalyticsPromptType {
case newUser(termsString: NSAttributedString)
case upgrade(termsString: NSAttributedString)
case newUser
case upgrade
}
extension AnalyticsPromptType {
@@ -67,11 +62,23 @@ extension AnalyticsPromptType {
}
}
/// The terms string that should be displayed.
var termsStrings: NSAttributedString {
/// The main part of the terms string that should be displayed.
var mainTermsString: String {
switch self {
case .newUser(let termsString), .upgrade(let termsString):
return termsString
case .newUser:
return VectorL10n.analyticsPromptTermsNewUser("%@")
case .upgrade:
return VectorL10n.analyticsPromptTermsUpgrade("%@")
}
}
/// The tappable part of the terms string that should be displayed.
var termsLinkString: String {
switch self {
case .newUser:
return VectorL10n.analyticsPromptTermsLinkNewUser
case .upgrade:
return VectorL10n.analyticsPromptTermsLinkUpgrade
}
}
@@ -96,15 +103,7 @@ extension AnalyticsPromptType {
}
}
extension AnalyticsPromptType: CaseIterable {
static var allCases: [AnalyticsPromptType] {
let strings = MockAnalyticsPromptStrings()
return [
.newUser(termsString: strings.termsNewUser),
.upgrade(termsString: strings.termsUpgrade)
]
}
}
extension AnalyticsPromptType: CaseIterable { }
extension AnalyticsPromptType: Identifiable {
var id: String {
@@ -52,9 +52,9 @@ final class AnalyticsPromptCoordinator: Coordinator, Presentable {
let promptType: AnalyticsPromptType
if Analytics.shared.promptShouldDisplayUpgradeMessage {
promptType = .upgrade(termsString: strings.termsUpgrade)
promptType = .upgrade
} else {
promptType = .newUser(termsString: strings.termsNewUser)
promptType = .newUser
}
let viewModel = AnalyticsPromptViewModel(promptType: promptType, strings: strings, termsURL: BuildSettings.analyticsTermsURL)
@@ -18,16 +18,7 @@ import Foundation
@available(iOS 14.0, *)
struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
let appDisplayName = AppInfo.current.displayName
let point1 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize)
let point2 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize)
let termsNewUser = HTMLFormatter().format(VectorL10n.analyticsPromptTermsNewUser("%@"),
with: VectorL10n.analyticsPromptTermsLinkNewUser,
using: BuildSettings.analyticsTermsURL)
let termsUpgrade = HTMLFormatter().format(VectorL10n.analyticsPromptTermsUpgrade("%@"),
with: VectorL10n.analyticsPromptTermsLinkUpgrade,
using: BuildSettings.analyticsTermsURL)
}
@@ -17,14 +17,9 @@
import UIKit
struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
var appDisplayName = "Element"
let point1: NSAttributedString
let point2: NSAttributedString
let termsNewUser: NSAttributedString
let termsUpgrade: NSAttributedString
let shortString = NSAttributedString(string: "This is a short string.")
let longString = NSAttributedString(string: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.")
@@ -38,15 +33,5 @@ struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
point2.append(NSAttributedString(string: "don't", attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)]))
point2.append(NSAttributedString(string: " share information with third parties"))
self.point2 = point2
let termsNewUser = NSMutableAttributedString(string: "You can read all our terms ")
termsNewUser.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!]))
termsNewUser.append(NSAttributedString(string: "."))
self.termsNewUser = termsNewUser
let termsUpgrade = NSMutableAttributedString(string: "Read all our terms ")
termsUpgrade.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!]))
termsUpgrade.append(NSAttributedString(string: ". Is that OK?"))
self.termsUpgrade = termsUpgrade
}
}
@@ -42,12 +42,10 @@ struct AnalyticsPrompt: View {
VStack {
Text("\(viewModel.viewState.promptType.message)\n")
AnalyticsPromptTermsText(attributedString: viewModel.viewState.promptType.termsStrings)
.accessibilityLabel(Text(viewModel.viewState.promptType.termsStrings.string))
.accessibilityValue(Text(VectorL10n.accessibilityButtonLabel))
.onTapGesture {
viewModel.send(viewAction: .openTermsURL)
}
InlineTextButton(viewModel.viewState.promptType.mainTermsString,
tappableText: viewModel.viewState.promptType.termsLinkString) {
viewModel.send(viewAction: .openTermsURL)
}
}
}
@@ -71,7 +69,7 @@ struct AnalyticsPrompt: View {
Image(uiImage: Asset.Images.analyticsLogo.image)
.padding(.bottom, 25)
Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.strings.appDisplayName))
Text(VectorL10n.analyticsPromptTitle(AppInfo.current.displayName))
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
.padding(.bottom, 2)
@@ -125,6 +123,7 @@ struct AnalyticsPrompt: View {
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
}
}
}
@@ -1,74 +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
@available(iOS 14.0, *)
/// The last line of text in the description with highlighting on the link string.
struct AnalyticsPromptTermsText: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme
/// A string with a link attribute.
private struct StringComponent {
let string: String
let isLink: Bool
}
/// Internal representation of the string as composable parts.
private let components: [StringComponent]
// MARK: - Setup
init(attributedString: NSAttributedString) {
var components = [StringComponent]()
let range = NSRange(location: 0, length: attributedString.length)
let string = attributedString.string as NSString
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in
let isLink = attributes.keys.contains(.link)
components.append(StringComponent(string: string.substring(with: range), isLink: isLink))
}
self.components = components
}
// MARK: - Views
var body: some View {
components.reduce(Text("")) {
$0 + Text($1.string).foregroundColor($1.isLink ? theme.colors.accent : nil)
}
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct AnalyticsPromptTermsText_Previews: PreviewProvider {
static let strings = MockAnalyticsPromptStrings()
static var previews: some View {
VStack(spacing: 8) {
AnalyticsPromptTermsText(attributedString: strings.termsNewUser)
AnalyticsPromptTermsText(attributedString: strings.termsUpgrade)
}
}
}
@@ -20,6 +20,7 @@ import Foundation
@available(iOS 14.0, *)
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
MockOnboardingUseCaseSelectionScreenState.self,
MockOnboardingSplashScreenScreenState.self,
MockLocationSharingScreenState.self,
MockAnalyticsPromptScreenState.self,
@@ -0,0 +1,89 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead that includes a link and handle the tap by adding an OpenURLAction to the environment.")
/// A `Button`, that fakes having a tappable string inside of a regular string.
struct InlineTextButton: View {
private struct StringComponent {
let string: Substring
let isTinted: Bool
}
// MARK: - Properties
// MARK: Private
/// The individual components of the string.
private let components: [StringComponent]
private let action: () -> Void
// MARK: - Setup
/// Creates a new `InlineTextButton`.
/// - Parameters:
/// - mainText: The main text that shouldn't appear tappable. This must contain a single `%@` placeholder somewhere within.
/// - tappableText: The tappable text that will be substituted into the `%@` placeholder.
/// - action: The action to perform when tapping the button.
internal init(_ mainText: String, tappableText: String, action: @escaping () -> Void) {
guard let range = mainText.range(of: "%@") else {
self.components = [StringComponent(string: Substring(mainText), isTinted: false)]
self.action = action
return
}
let firstComponent = StringComponent(string: mainText[..<range.lowerBound], isTinted: false)
let middleComponent = StringComponent(string: Substring(tappableText), isTinted: true)
let lastComponent = StringComponent(string: mainText[range.upperBound...], isTinted: false)
self.components = [firstComponent, middleComponent, lastComponent]
self.action = action
}
// MARK: - Views
var body: some View {
Button(action: action) {
EmptyView()
}
.buttonStyle(Style(components: components))
.accessibilityLabel(components.map { $0.string }.joined())
}
private struct Style: ButtonStyle {
let components: [StringComponent]
func makeBody(configuration: Configuration) -> some View {
components.reduce(Text("")) { lastValue, component in
lastValue + Text(component.string)
.foregroundColor(component.isTinted ? .accentColor.opacity(configuration.isPressed ? 0.2 : 1) : nil)
}
}
}
}
@available(iOS 14.0, *)
struct Previews_InlineButtonText_Previews: PreviewProvider {
static var previews: some View {
InlineTextButton("Hello there this is a sentence. %@.",
tappableText: "And this is a button",
action: { })
.padding()
}
}
@@ -0,0 +1,126 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
import DesignKit
@available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead.")
/// A `Text` view that renders attributed strings with their `.font` and `.foregroundColor` attributes.
/// This view is a workaround for iOS 13/14 not supporting `AttributedString`.
struct StyledText: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme
/// A string with a bold property.
private struct StringComponent {
let string: String
var font: Font? = nil
var color: Color? = nil
}
/// Internal representation of the string as composable parts.
private let components: [StringComponent]
// MARK: - Setup
/// Creates a `StyledText` using the supplied attributed string.
/// - Parameter attributedString: The attributed string to display.
init(_ attributedString: NSAttributedString) {
var components = [StringComponent]()
let range = NSRange(location: 0, length: attributedString.length)
let string = attributedString.string as NSString
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in
let font = attributes[.font] as? UIFont
let color = attributes[.foregroundColor] as? UIColor
let component = StringComponent(
string: string.substring(with: range),
font: font.map { Font($0) },
color: color.map { Color($0) }
)
components.append(component)
}
self.components = components
}
/// Creates a `StyledText` using a plain string.
/// - Parameter string: The plain string to display
init(_ string: String) {
self.components = [StringComponent(string: string, font: nil)]
}
// MARK: - Views
var body: some View {
components.reduce(Text("")) { lastValue, component in
lastValue + Text(component.string)
.font(component.font)
.foregroundColor(component.color)
}
}
}
@available(iOS 14.0, *)
struct StyledText_Previews: PreviewProvider {
static func prettyText() -> NSAttributedString {
let string = NSMutableAttributedString(string: "T", attributes: [
.font: UIFont.boldSystemFont(ofSize: 12),
.foregroundColor: UIColor.red
])
string.append(NSAttributedString(string: "e", attributes: [
.font: UIFont.boldSystemFont(ofSize: 14),
.foregroundColor: UIColor.orange
]))
string.append(NSAttributedString(string: "s", attributes: [
.font: UIFont.boldSystemFont(ofSize: 13),
.foregroundColor: UIColor.yellow
]))
string.append(NSAttributedString(string: "t", attributes: [
.font: UIFont.boldSystemFont(ofSize: 15),
.foregroundColor: UIColor.green
]))
string.append(NSAttributedString(string: "i", attributes: [
.font: UIFont.boldSystemFont(ofSize: 11),
.foregroundColor: UIColor.cyan
]))
string.append(NSAttributedString(string: "n", attributes: [
.font: UIFont.boldSystemFont(ofSize: 16),
.foregroundColor: UIColor.blue
]))
string.append(NSAttributedString(string: "g", attributes: [
.font: UIFont.boldSystemFont(ofSize: 14),
.foregroundColor: UIColor.purple
]))
return string
}
static var previews: some View {
VStack(spacing: 8) {
StyledText("Hello, World!")
StyledText(NSAttributedString(string: "Testing",
attributes: [.font: UIFont.boldSystemFont(ofSize: 64)]))
StyledText(prettyText())
}
}
}
@@ -0,0 +1,32 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct OnboardingButtonStyle: ButtonStyle {
@Environment(\.theme) private var theme
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 8)
.stroke(configuration.isPressed ? theme.colors.accent : theme.colors.quinaryContent, lineWidth: configuration.isPressed ? 2 : 1.5)
)
.contentShape(RoundedRectangle(cornerRadius: 8))
}
}
@@ -48,6 +48,7 @@ struct OnboardingSplashScreenPage: View {
.scaledToFit()
.frame(maxWidth: 300)
.padding(20)
.accessibilityHidden(true)
VStack(spacing: 8) {
OnboardingSplashScreenTitleText(content.title)
@@ -0,0 +1,61 @@
//
// 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
final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let onboardingUseCaseHostingController: UIViewController
private var onboardingUseCaseViewModel: OnboardingUseCaseViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: ((OnboardingUseCaseViewModelResult) -> Void)?
// MARK: - Setup
@available(iOS 14.0, *)
init() {
let viewModel = OnboardingUseCaseViewModel()
let view = OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
onboardingUseCaseViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.vc_removeBackTitle()
hostingController.enableNavigationBarScrollEdgesAppearance = true
onboardingUseCaseHostingController = hostingController
}
// MARK: - Public
func start() {
MXLog.debug("[OnboardingUseCaseSelectionCoordinator] did start.")
onboardingUseCaseViewModel.completion = { [weak self] result in
MXLog.debug("[OnboardingUseCaseSelectionCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).")
guard let self = self else { return }
self.completion?(result)
}
}
func toPresentable() -> UIViewController {
return self.onboardingUseCaseHostingController
}
}
@@ -0,0 +1,52 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
@available(iOS 14.0, *)
enum MockOnboardingUseCaseSelectionScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case `default`
/// The associated screen
var screenType: Any.Type {
OnboardingUseCaseSelectionScreen.self
}
/// A list of screen state definitions
static var allCases: [MockOnboardingUseCaseSelectionScreenState] {
// Each of the presence statuses
[.default]
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let viewModel = OnboardingUseCaseViewModel()
// can simulate service and viewModel actions here if needs be.
return (
[self, viewModel],
AnyView(OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
.addDependency(MockAvatarService.example))
)
}
}
@@ -0,0 +1,41 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
// MARK: - Coordinator
// MARK: View model
enum OnboardingUseCaseStateAction {
case viewAction(OnboardingUseCaseViewAction)
}
enum OnboardingUseCaseViewModelResult {
case personalMessaging
case workMessaging
case communityMessaging
case skipped
case customServer
}
// MARK: View
struct OnboardingUseCaseViewState: BindableState { }
enum OnboardingUseCaseViewAction {
case answer(OnboardingUseCaseViewModelResult)
}
@@ -0,0 +1,52 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14, *)
typealias OnboardingUseCaseViewModelType = StateStoreViewModel<OnboardingUseCaseViewState,
OnboardingUseCaseStateAction,
OnboardingUseCaseViewAction>
@available(iOS 14, *)
class OnboardingUseCaseViewModel: OnboardingUseCaseViewModelType, OnboardingUseCaseViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var completion: ((OnboardingUseCaseViewModelResult) -> Void)?
// MARK: - Setup
init() {
super.init(initialViewState: OnboardingUseCaseViewState())
}
// MARK: - Public
override func process(viewAction: OnboardingUseCaseViewAction) {
switch viewAction {
case .answer(let result):
completion?(result)
}
}
override class func reducer(state: inout OnboardingUseCaseViewState, action: OnboardingUseCaseStateAction) {
// There is no mutable state to reduce :)
}
}
@@ -0,0 +1,24 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol OnboardingUseCaseViewModelProtocol {
var completion: ((OnboardingUseCaseViewModelResult) -> Void)? { get set }
@available(iOS 14, *)
var context: OnboardingUseCaseViewModelType.Context { get }
}
@@ -0,0 +1,23 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import RiotSwiftUI
@available(iOS 14.0, *)
class OnboardingUseCaseUITests: MockScreenTest {
// The view has no parameters or changing state to test.
}
@@ -0,0 +1,24 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import RiotSwiftUI
@available(iOS 14.0, *)
class OnboardingUseCaseViewModelTests: XCTestCase {
// The view model has nothing to test.
}
@@ -0,0 +1,59 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
/// A button used for the Use Case selection.
struct OnboardingUseCaseButton: View {
// MARK: Private
@Environment(\.theme) private var theme
// MARK: Public
/// The button's title.
let title: String
/// The button's image.
let image: ImageAsset
/// The button's action when tapped.
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 16) {
Image(image.name)
Text(title)
.font(theme.fonts.bodySB)
.foregroundColor(theme.colors.primaryContent)
}
.padding(16)
}
.buttonStyle(OnboardingButtonStyle())
}
}
@available(iOS 14.0, *)
struct Previews_OnboardingUseCaseButton_Previews: PreviewProvider {
static var previews: some View {
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCaseWorkMessaging,
image: Asset.Images.onboardingUseCaseWork,
action: { })
.padding(16)
}
}
@@ -0,0 +1,130 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
/// The screen shown to a new user to select their use case for the app.
struct OnboardingUseCaseSelectionScreen: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme
// MARK: Public
@ObservedObject var viewModel: OnboardingUseCaseViewModel.Context
/// The screen's title and instructions.
var titleContent: some View {
VStack(spacing: 8) {
Image(Asset.Images.onboardingUseCaseIcon.name)
.padding(.bottom, 8)
.accessibilityHidden(true)
Text(VectorL10n.onboardingUseCaseTitle)
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
Text(VectorL10n.onboardingUseCaseMessage)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
}
}
/// The buttons used to select a use case for the app.
var useCaseButtons: some View {
VStack(spacing: 8) {
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCasePersonalMessaging,
image: theme.isDark ? Asset.Images.onboardingUseCasePersonalDark : Asset.Images.onboardingUseCasePersonal) {
viewModel.send(viewAction: .answer(.personalMessaging))
}
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCaseWorkMessaging,
image: theme.isDark ? Asset.Images.onboardingUseCaseWorkDark : Asset.Images.onboardingUseCaseWork) {
viewModel.send(viewAction: .answer(.workMessaging))
}
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCaseCommunityMessaging,
image: theme.isDark ? Asset.Images.onboardingUseCaseCommunityDark : Asset.Images.onboardingUseCaseCommunity) {
viewModel.send(viewAction: .answer(.communityMessaging))
}
InlineTextButton(VectorL10n.onboardingUseCaseNotSureYet("%@"),
tappableText: VectorL10n.onboardingUseCaseSkipButton) {
viewModel.send(viewAction: .answer(.skipped))
}
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.tertiaryContent)
.padding(.top, 8)
}
}
/// A footer showing a button to connect to a server.
var serverFooter: some View {
VStack(spacing: 14) {
Text(VectorL10n.onboardingUseCaseExistingServerMessage)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.tertiaryContent)
Button { viewModel.send(viewAction: .answer(.customServer)) } label: {
Text(VectorL10n.onboardingUseCaseExistingServerButton)
.font(theme.fonts.body)
}
}
}
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
VStack(spacing: 0) {
titleContent
.padding(.bottom, 36)
useCaseButtons
}
.frame(maxWidth: OnboardingConstants.maxContentWidth,
maxHeight: OnboardingConstants.maxContentHeight)
.padding(16)
}
.frame(maxWidth: .infinity)
serverFooter
.padding(.horizontal, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36)
}
}
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct OnboardingUseCase_Previews: PreviewProvider {
static let stateRenderer = MockOnboardingUseCaseSelectionScreenState.stateRenderer
static var previews: some View {
NavigationView {
stateRenderer.screenGroup()
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
@@ -97,7 +97,8 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
static func shareLocationActivityController(_ location: CLLocationCoordinate2D) -> UIActivityViewController {
return UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)],
applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location),
ShareToMapsAppActivity(type: .google, location: location)])
ShareToMapsAppActivity(type: .google, location: location),
ShareToMapsAppActivity(type: .osm, location: location)])
}
// MARK: - Presentable
@@ -24,6 +24,7 @@ class ShareToMapsAppActivity: UIActivity {
enum MapsAppType {
case apple
case google
case osm
}
private let type: MapsAppType
@@ -44,6 +45,8 @@ class ShareToMapsAppActivity: UIActivity {
return URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin")!
case .google:
return URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)")!
case .osm:
return URL(string: "https://www.openstreetmap.org/?mlat=\(location.latitude)&mlon=\(location.longitude)")!
}
}
@@ -53,6 +56,8 @@ class ShareToMapsAppActivity: UIActivity {
return VectorL10n.locationSharingOpenAppleMaps
case .google:
return VectorL10n.locationSharingOpenGoogleMaps
case .osm:
return VectorL10n.locationSharingOpenOpenStreetMaps
}
}
@@ -32,66 +32,74 @@ struct LocationSharingView: View {
var body: some View {
NavigationView {
LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL,
avatarData: context.viewState.avatarData,
location: context.viewState.location,
errorSubject: context.viewState.errorSubject,
userLocation: $context.userLocation)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(VectorL10n.cancel, action: {
context.send(viewAction: .cancel)
})
}
ToolbarItem(placement: .principal) {
Text(VectorL10n.locationSharingTitle)
.font(.headline)
.foregroundColor(theme.colors.primaryContent)
}
ToolbarItem(placement: .navigationBarTrailing) {
if context.viewState.location != nil {
Button {
context.send(viewAction: .share)
} label: {
Image(uiImage: Asset.Images.locationShareIcon.image)
.accessibilityIdentifier("LocationSharingView.shareButton")
}
.disabled(!context.viewState.shareButtonEnabled)
} else {
Button(VectorL10n.locationSharingShareAction, action: {
context.send(viewAction: .share)
})
.disabled(!context.viewState.shareButtonEnabled)
ZStack(alignment: .bottom) {
LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL,
avatarData: context.viewState.avatarData,
location: context.viewState.location,
errorSubject: context.viewState.errorSubject,
userLocation: $context.userLocation)
.ignoresSafeArea()
HStack {
Link("© MapTiler", destination: URL(string: "https://www.maptiler.com/copyright/")!)
Link("© OpenStreetMap contributors", destination: URL(string: "https://www.openstreetmap.org/copyright")!)
}
.font(theme.fonts.caption1)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(VectorL10n.cancel, action: {
context.send(viewAction: .cancel)
})
}
ToolbarItem(placement: .principal) {
Text(VectorL10n.locationSharingTitle)
.font(.headline)
.foregroundColor(theme.colors.primaryContent)
}
ToolbarItem(placement: .navigationBarTrailing) {
if context.viewState.location != nil {
Button {
context.send(viewAction: .share)
} label: {
Image(uiImage: Asset.Images.locationShareIcon.image)
.accessibilityIdentifier("LocationSharingView.shareButton")
}
}
}
.navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea()
.alert(item: $context.alertInfo) { info in
if let secondaryButton = info.secondaryButton {
return Alert(title: Text(info.title),
message: subtitleTextForAlertInfo(info),
primaryButton: .default(Text(info.primaryButton.title)) {
info.primaryButton.action?()
},
secondaryButton: .default(Text(secondaryButton.title)) {
secondaryButton.action?()
})
.disabled(!context.viewState.shareButtonEnabled)
} else {
return Alert(title: Text(info.title),
message: subtitleTextForAlertInfo(info),
dismissButton: .default(Text(info.primaryButton.title)) {
info.primaryButton.action?()
})
Button(VectorL10n.locationSharingShareAction, action: {
context.send(viewAction: .share)
})
.disabled(!context.viewState.shareButtonEnabled)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.introspectNavigationController { navigationController in
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
}
.alert(item: $context.alertInfo) { info in
if let secondaryButton = info.secondaryButton {
return Alert(title: Text(info.title),
message: subtitleTextForAlertInfo(info),
primaryButton: .default(Text(info.primaryButton.title)) {
info.primaryButton.action?()
},
secondaryButton: .default(Text(secondaryButton.title)) {
secondaryButton.action?()
})
} else {
return Alert(title: Text(info.title),
message: subtitleTextForAlertInfo(info),
dismissButton: .default(Text(info.primaryButton.title)) {
info.primaryButton.action?()
})
}
}
}
.accentColor(theme.colors.accent)
.activityIndicator(show: context.viewState.showLoadingIndicator)
.navigationViewStyle(StackNavigationViewStyle())
.introspectNavigationController { navigationController in
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
}
}
@ViewBuilder
@@ -146,15 +146,17 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
}
private static func pollDetailsTypeToKindKey(_ type: EditFormPollType) -> String {
let mapping = [EditFormPollType.disclosed : kMXMessageContentKeyExtensiblePollKindDisclosed,
EditFormPollType.undisclosed : kMXMessageContentKeyExtensiblePollKindUndisclosed]
let mapping = [EditFormPollType.disclosed : kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381,
EditFormPollType.undisclosed : kMXMessageContentKeyExtensiblePollKindUndisclosedMSC3381]
return mapping[type] ?? kMXMessageContentKeyExtensiblePollKindDisclosed
return mapping[type] ?? kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381
}
private static func pollKindKeyToDetailsType(_ key: String) -> EditFormPollType {
let mapping = [kMXMessageContentKeyExtensiblePollKindDisclosed : EditFormPollType.disclosed,
kMXMessageContentKeyExtensiblePollKindUndisclosed : EditFormPollType.undisclosed]
kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381 : EditFormPollType.disclosed,
kMXMessageContentKeyExtensiblePollKindUndisclosed : EditFormPollType.undisclosed,
kMXMessageContentKeyExtensiblePollKindUndisclosedMSC3381 : EditFormPollType.undisclosed]
return mapping[key] ?? EditFormPollType.disclosed
}
@@ -35,8 +35,7 @@ struct PollEditForm: View {
ScrollView {
VStack(alignment: .leading, spacing: 32.0) {
// Intentionally disabled until platform parity.
// PollEditFormTypePicker(selectedType: $viewModel.type)
PollEditFormTypePicker(selectedType: $viewModel.type)
VStack(alignment: .leading, spacing: 16.0) {
Text(VectorL10n.pollEditFormPollQuestionOrTopic)
@@ -116,14 +115,14 @@ struct PollEditForm: View {
}
}
.navigationBarTitleDisplayMode(.inline)
.introspectNavigationController { navigationController in
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
}
}
}
}
.accentColor(theme.colors.accent)
.navigationViewStyle(StackNavigationViewStyle())
.introspectNavigationController { navigationController in
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
}
}
}
@@ -16,6 +16,7 @@
import Foundation
import Combine
import MatrixSDK
@available(iOS 14.0, *)
class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
@@ -25,7 +26,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
// MARK: Private
private let roomId: String
private let session:MXSession
private let session: MXSession
private var replacementRoom: MXRoom?
private var didBuildSpaceGraphObserver: Any?
private var accessItems: [RoomAccessTypeChooserAccessItem] = []
@@ -66,7 +67,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
self.roomId = roomId
self.session = session
self.currentRoomId = roomId
restrictedVersionOverride = session.homeserverCapabilities.versionOverrideForFeature(.restricted)
restrictedVersionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted)
roomUpgradeRequiredSubject = CurrentValueSubject(false)
waitingMessageSubject = CurrentValueSubject(nil)
@@ -193,7 +194,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
room.state { [weak self] state in
guard let self = self else { return }
if let roomVersion = state?.stateEvents(with: .roomCreate)?.last?.wireContent["room_version"] as? String, let homeserverCapabilitiesService = self.session.homeserverCapabilities {
if let roomVersion = state?.stateEvents(with: .roomCreate)?.last?.wireContent["room_version"] as? String, let homeserverCapabilitiesService = self.session.homeserverCapabilitiesService {
self.roomUpgradeRequired = self.restrictedVersionOverride != nil && !homeserverCapabilitiesService.isFeatureSupported(.restricted, by: roomVersion)
}
@@ -30,23 +30,6 @@ class RoomAccessTypeChooserUITests: MockScreenTest {
func verifyRoomAccessTypeChooserScreen() throws {
guard let screenState = screenState as? MockRoomAccessTypeChooserScreenState else { fatalError("no screen") }
switch screenState {
case .noRooms:
verifyRoomAccessTypeChooserNoRooms()
case .rooms:
verifyRoomAccessTypeChooserRooms()
}
}
func verifyRoomAccessTypeChooserNoRooms() {
let errorMessage = app.staticTexts["errorMessage"]
XCTAssert(errorMessage.exists)
XCTAssert(errorMessage.label == "No Rooms")
}
func verifyRoomAccessTypeChooserRooms() {
let displayNameCount = app.buttons.matching(identifier:"roomNameText").count
XCTAssertEqual(displayNameCount, 3)
}
}
@@ -96,8 +96,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
}
func canEditPoll() -> Bool {
return false // Intentionally disabled until platform parity.
// return pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0
return pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0
}
func endPoll() {