Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FTUE celebration screen #5891

Merged
merged 3 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_celebration_icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions Riot/Assets/en.lproj/Untranslated.strings
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@
"onboarding_avatar_message" = "You can change this anytime.";
"onboarding_avatar_accessibility_label" = "Profile picture";

"onboarding_celebration_title" = "You’re all set!";
"onboarding_celebration_message" = "Your preferences have been saved.";
"onboarding_celebration_button" = "Let's go";

"image_picker_action_files" = "Choose from files";
1 change: 1 addition & 0 deletions Riot/Generated/Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ internal class Asset: NSObject {
internal static let onboardingSplashScreenPage4Dark = ImageAsset(name: "OnboardingSplashScreenPage4Dark")
internal static let onboardingAvatarCamera = ImageAsset(name: "onboarding_avatar_camera")
internal static let onboardingAvatarEdit = ImageAsset(name: "onboarding_avatar_edit")
internal static let onboardingCelebrationIcon = ImageAsset(name: "onboarding_celebration_icon")
internal static let onboardingCongratulationsIcon = ImageAsset(name: "onboarding_congratulations_icon")
internal static let onboardingUseCaseCommunity = ImageAsset(name: "onboarding_use_case_community")
internal static let onboardingUseCaseCommunityDark = ImageAsset(name: "onboarding_use_case_community_dark")
Expand Down
14 changes: 13 additions & 1 deletion Riot/Generated/UntranslatedStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,24 @@ public extension VectorL10n {
static var onboardingAvatarTitle: String {
return VectorL10n.tr("Untranslated", "onboarding_avatar_title")
}
/// Let's go
static var onboardingCelebrationButton: String {
return VectorL10n.tr("Untranslated", "onboarding_celebration_button")
}
/// Your preferences have been saved.
static var onboardingCelebrationMessage: String {
return VectorL10n.tr("Untranslated", "onboarding_celebration_message")
}
/// You’re all set!
static var onboardingCelebrationTitle: String {
return VectorL10n.tr("Untranslated", "onboarding_celebration_title")
}
/// Take me home
static var onboardingCongratulationsHomeButton: String {
return VectorL10n.tr("Untranslated", "onboarding_congratulations_home_button")
}
/// Your account %@ has been created.
public static func onboardingCongratulationsMessage(_ p1: String) -> String {
static func onboardingCongratulationsMessage(_ p1: String) -> String {
return VectorL10n.tr("Untranslated", "onboarding_congratulations_message", p1)
}
/// Personalise profile
Expand Down
34 changes: 27 additions & 7 deletions Riot/Modules/Onboarding/OnboardingCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -391,14 +391,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
private func displayNameCoordinator(_ coordinator: OnboardingDisplayNameCoordinator, didCompleteWith userSession: UserSession) {
if shouldShowAvatarScreen {
showAvatarScreen(for: userSession)
return
} else if Analytics.shared.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(for: userSession.matrixSession)
return
} else {
showCelebrationScreen(for: userSession)
}

onboardingFinished = true
completeIfReady()
}

/// Show the avatar personalization screen for new users using the supplied user session.
Expand Down Expand Up @@ -431,6 +426,31 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
/// Displays the next view in the flow after the avatar screen.
@available(iOS 14.0, *)
private func avatarCoordinator(_ coordinator: OnboardingAvatarCoordinator, didCompleteWith userSession: UserSession) {
showCelebrationScreen(for: userSession)
}

@available(iOS 14.0, *)
private func showCelebrationScreen(for userSession: UserSession) {
MXLog.debug("[OnboardingCoordinator] showCelebrationScreen")

let parameters = OnboardingCelebrationCoordinatorParameters(userSession: userSession)
let coordinator = OnboardingCelebrationCoordinator(parameters: parameters)

coordinator.completion = { [weak self, weak coordinator] userSession in
guard let self = self, let coordinator = coordinator else { return }
self.celebrationCoordinator(coordinator, didCompleteWith: userSession)
}

add(childCoordinator: coordinator)
coordinator.start()

navigationRouter.setRootModule(coordinator, hideNavigationBar: true, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}

@available(iOS 14.0, *)
private func celebrationCoordinator(_ coordinator: OnboardingCelebrationCoordinator, didCompleteWith userSession: UserSession) {
if Analytics.shared.shouldShowAnalyticsPrompt {
showAnalyticsPrompt(for: userSession.matrixSession)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ struct AnalyticsPrompt: View {
.padding(.top, 50)
.padding(.horizontal, horizontalPadding)
}
.frame(maxWidth: OnboardingConstants.maxContentWidth)
.frame(maxWidth: OnboardingMetrics.maxContentWidth)
.frame(maxWidth: .infinity)

buttons
.frame(maxWidth: OnboardingConstants.maxContentWidth)
.frame(maxWidth: OnboardingMetrics.maxContentWidth)
.padding(.horizontal, horizontalPadding)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
Expand Down
Binary file not shown.
78 changes: 78 additions & 0 deletions RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// 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 SceneKit
import SwiftUI

@available(iOS 14.0, *)
class EffectsScene: SCNScene {

// MARK: - Constants

private enum Constants {
static let confettiSceneName = "ConfettiScene.scn"
static let particlesNodeName = "particles"
}

// MARK: - Public

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 }

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
particles.particleColorVariation = SCNVector4(x: 0, y: 0, z: 0, w: 0.1)

// Add a handler to customize the color of the particles.
particles.handle(.birth, forProperties: [.color]) { data, dataStride, indices, count in
for index in 0..<count {
// Pick a random color to apply to the particle.
guard let color = colors.randomElement() else { continue }

// Get the particle's color pointer.
let colorPointer = data[0] + dataStride[0] * index
let rgbaPointer = colorPointer.bindMemory(to: Float.self, capacity: dataStride[0])

// Update the color for the particle.
rgbaPointer[0] = color[0]
rgbaPointer[1] = color[1]
rgbaPointer[2] = color[2]
rgbaPointer[3] = 1
}
}
}

return scene
}
}

@available(iOS 14.0, *)
fileprivate extension Color {
/// The color's components as an array of floats in the extended linear sRGB colorspace.
///
/// SceneKit works in a colorspace with a linear gamma, which is why this conversion is necessary.
var floatComponents: [Float]? {
guard
let colorSpace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB),
let linearColor = cgColor?.converted(to: colorSpace, intent: .defaultIntent, options: nil),
let components = linearColor.components
else { return nil }

return components.map { Float($0) }
}
}
62 changes: 62 additions & 0 deletions RiotSwiftUI/Modules/Common/EffectsScene/EffectsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// 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
import SceneKit

@available(iOS 14.0, *)
/// A SwiftUI wrapper around `SCNView`, that unlike `SceneView` allows the
/// scene to have a transparent background and be rendered on top of other views.
struct EffectsView: UIViewRepresentable {

// MARK: - Private

@Environment(\.theme) private var theme

// MARK: - Public

enum EffectsType {
/// A confetti drop effect from the top centre of the screen.
case confetti
/// No effect will be shown.
case none
}

/// The type of effects to be shown in the view.
var effectsType: EffectsType = .none

// MARK: - Lifecycle

func makeUIView(context: Context) -> SCNView {
SCNView(frame: .zero)
}

func updateUIView(_ sceneView: SCNView, context: Context) {
sceneView.scene = makeScene()
sceneView.backgroundColor = .clear
}

// MARK: - Private

private func makeScene() -> EffectsScene? {
switch effectsType {
case .confetti:
return EffectsScene.confetti(with: theme)
case .none:
return nil
}
}
}
1 change: 1 addition & 0 deletions RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Foundation
@available(iOS 14.0, *)
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
MockOnboardingCelebrationScreenState.self,
MockOnboardingAvatarScreenState.self,
MockOnboardingDisplayNameScreenState.self,
MockOnboardingCongratulationsScreenState.self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ struct OnboardingAvatarScreen: View {
}
.padding(.horizontal)
.padding(.top, 8)
.frame(maxWidth: OnboardingConstants.maxContentWidth)
.frame(maxWidth: OnboardingMetrics.maxContentWidth)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accentColor(theme.colors.accent)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// 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

struct OnboardingCelebrationCoordinatorParameters {
let userSession: UserSession
}

@available(iOS 14.0, *)
final class OnboardingCelebrationCoordinator: Coordinator, Presentable {

// MARK: - Properties

// MARK: Private

private let parameters: OnboardingCelebrationCoordinatorParameters
private let onboardingCelebrationHostingController: VectorHostingController
private var onboardingCelebrationViewModel: OnboardingCelebrationViewModelProtocol

// MARK: Public

// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: ((UserSession) -> Void)?

// MARK: - Setup

init(parameters: OnboardingCelebrationCoordinatorParameters) {
self.parameters = parameters

let viewModel = OnboardingCelebrationViewModel()
let view = OnboardingCelebrationScreen(viewModel: viewModel.context)
onboardingCelebrationViewModel = viewModel
onboardingCelebrationHostingController = VectorHostingController(rootView: view)
onboardingCelebrationHostingController.enableNavigationBarScrollEdgeAppearance = true
}

// MARK: - Public
func start() {
MXLog.debug("[OnboardingCelebrationCoordinator] did start.")
onboardingCelebrationViewModel.completion = { [weak self] in
guard let self = self else { return }
MXLog.debug("[OnboardingCelebrationCoordinator] OnboardingCelebrationViewModel did complete.")
self.completion?(self.parameters.userSession)
}
}

func toPresentable() -> UIViewController {
return self.onboardingCelebrationHostingController
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// 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 MockOnboardingCelebrationScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case confetti

/// The associated screen
var screenType: Any.Type {
OnboardingCelebrationScreen.self
}

/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let viewModel = OnboardingCelebrationViewModel()

// can simulate service and viewModel actions here if needs be.

return (
[self, viewModel],
AnyView(OnboardingCelebrationScreen(viewModel: viewModel.context)
.addDependency(MockAvatarService.example))
)
}
}
Loading