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 informational sheet for sessions (PSG-714) #6992

Merged
merged 40 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f2e5506
Add empty onLearnMoreAction closure
alfogrillo Oct 25, 2022
503db0a
Add InfoView skeleton
alfogrillo Oct 26, 2022
ce61fc1
Add UserSessionOverviewViewBindings
alfogrillo Oct 26, 2022
87d642d
Style info view
alfogrillo Oct 26, 2022
6058149
Add bottom sheet modifier
alfogrillo Oct 26, 2022
a28de2b
Localise content
alfogrillo Oct 26, 2022
0210dcc
Add inactive sessions copy
alfogrillo Oct 26, 2022
57717ac
Fix bug in InlineTextButton
alfogrillo Oct 26, 2022
c6342a3
Improve UserSessionCardView
alfogrillo Oct 26, 2022
8ababcf
Add “learn more” button in UserOtherSessions
alfogrillo Oct 26, 2022
55ab2ab
Show bottom sheet in user other sessions
alfogrillo Oct 26, 2022
5504a16
Show rename info alert
alfogrillo Oct 26, 2022
8fec660
Refine UX
alfogrillo Oct 26, 2022
a6b35c7
Add iOS 15- fallback
alfogrillo Oct 26, 2022
f73a187
Refine InfoView
alfogrillo Oct 26, 2022
e2f4fde
Add UI tests
alfogrillo Oct 26, 2022
8948f00
Improve UserOtherSessionsUITests
alfogrillo Oct 26, 2022
ca446d3
Improve InlineTextButton API
alfogrillo Oct 26, 2022
9f38021
Add changelod.d file
alfogrillo Oct 27, 2022
8c041fa
Fix failing UTs
alfogrillo Oct 27, 2022
7cd944e
Hide keyboard in UserSessionName
alfogrillo Oct 27, 2022
a9a7b11
Add .viewSessionInfo view action
alfogrillo Oct 28, 2022
93c24dd
Add MVVM-C for InfoSheet
alfogrillo Oct 28, 2022
6cb07dd
Show bottom sheet in other sessions screen
alfogrillo Oct 28, 2022
acd58d7
Show bottom sheet in rename session screen
alfogrillo Oct 28, 2022
7ab8038
Delete bottom sheet modifier
alfogrillo Oct 28, 2022
6d7c53d
Show rename sheet
alfogrillo Oct 28, 2022
ebd30b1
Add InfoSheet SwiftUI preview
alfogrillo Oct 28, 2022
9b8f158
Fix memory leak
alfogrillo Oct 28, 2022
3b8e8d1
Cleanup UI tests
alfogrillo Oct 28, 2022
24ed60c
Merge branch 'develop' into alfogrillo/learn_more_sheet
alfogrillo Nov 3, 2022
d0056ed
Cleanup
alfogrillo Nov 3, 2022
3678a52
Fix failing test
alfogrillo Nov 3, 2022
09e9f3e
Merge branch 'develop' into alfogrillo/learn_more_sheet
alfogrillo Nov 3, 2022
455118d
Merge branch 'develop' into alfogrillo/learn_more_sheet
alfogrillo Nov 3, 2022
5b73438
Amend title font
alfogrillo Nov 4, 2022
d1560de
Amend copies
alfogrillo Nov 4, 2022
d5c3bd2
Merge branch 'develop' into alfogrillo/learn_more_sheet
alfogrillo Nov 8, 2022
ab00c7e
Refine bottom sheet layout
alfogrillo Nov 8, 2022
794a071
Add intrinsic sized bottom sheet
alfogrillo Nov 9, 2022
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
9 changes: 9 additions & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2448,6 +2448,15 @@ To enable access, tap Settings> Location and select Always";
"user_other_session_verified_additional_info" = "This session is ready for secure messaging.";
"user_session_push_notifications" = "Push notifications";
"user_session_push_notifications_message" = "When turned on, this session will receive push notifications.";
"user_session_got_it" = "Got it";
"user_session_verified_session_title" = "Verified sessions";
"user_session_verified_session_description" = "Verified sessions are anywhere you are using Element after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.";
"user_session_unverified_session_title" = "Unverified session";
"user_session_unverified_session_description" = "Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.";
"user_session_inactive_session_title" = "Inactive sessions";
"user_session_inactive_session_description" = "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.";
"user_session_rename_session_title" = "Renaming sessions";
"user_session_rename_session_description" = "Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.";

"user_other_session_security_recommendation_title" = "Security recommendation";
"user_other_session_unverified_sessions_header_subtitle" = "Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore.";
Expand Down
36 changes: 36 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8787,6 +8787,18 @@ public class VectorL10n: NSObject {
public static var userSessionDetailsTitle: String {
return VectorL10n.tr("Vector", "user_session_details_title")
}
/// Got it
public static var userSessionGotIt: String {
return VectorL10n.tr("Vector", "user_session_got_it")
}
/// Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.
public static var userSessionInactiveSessionDescription: String {
return VectorL10n.tr("Vector", "user_session_inactive_session_description")
}
/// Inactive sessions
public static var userSessionInactiveSessionTitle: String {
return VectorL10n.tr("Vector", "user_session_inactive_session_title")
}
/// %1$@ · %2$@
public static func userSessionItemDetails(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "user_session_item_details", p1, p2)
Expand Down Expand Up @@ -8823,6 +8835,14 @@ public class VectorL10n: NSObject {
public static var userSessionPushNotificationsMessage: String {
return VectorL10n.tr("Vector", "user_session_push_notifications_message")
}
/// Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.
public static var userSessionRenameSessionDescription: String {
return VectorL10n.tr("Vector", "user_session_rename_session_description")
}
/// Renaming sessions
public static var userSessionRenameSessionTitle: String {
return VectorL10n.tr("Vector", "user_session_rename_session_title")
}
/// Unverified session
public static var userSessionUnverified: String {
return VectorL10n.tr("Vector", "user_session_unverified")
Expand All @@ -8831,6 +8851,14 @@ public class VectorL10n: NSObject {
public static var userSessionUnverifiedAdditionalInfo: String {
return VectorL10n.tr("Vector", "user_session_unverified_additional_info")
}
/// Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.
public static var userSessionUnverifiedSessionDescription: String {
return VectorL10n.tr("Vector", "user_session_unverified_session_description")
}
/// Unverified session
public static var userSessionUnverifiedSessionTitle: String {
return VectorL10n.tr("Vector", "user_session_unverified_session_title")
}
/// Unverified
public static var userSessionUnverifiedShort: String {
return VectorL10n.tr("Vector", "user_session_unverified_short")
Expand All @@ -8855,6 +8883,14 @@ public class VectorL10n: NSObject {
public static var userSessionVerifiedAdditionalInfo: String {
return VectorL10n.tr("Vector", "user_session_verified_additional_info")
}
/// Verified sessions are anywhere you are using Element after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.
public static var userSessionVerifiedSessionDescription: String {
return VectorL10n.tr("Vector", "user_session_verified_session_description")
}
/// Verified sessions
public static var userSessionVerifiedSessionTitle: String {
return VectorL10n.tr("Vector", "user_session_verified_session_title")
}
/// Verified
public static var userSessionVerifiedShort: String {
return VectorL10n.tr("Vector", "user_session_verified_short")
Expand Down
24 changes: 24 additions & 0 deletions RiotSwiftUI/Modules/Common/Extensions/View.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// 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 SwiftUI

extension View {
func hideKeyboard() {
UIApplication.shared.vc_closeKeyboard()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// 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 CommonKit
import SwiftUI

struct InfoSheetCoordinatorParameters {
let title: String
let description: String
let action: InfoSheet.Action
let parentSize: CGSize?
}

final class InfoSheetCoordinator: Coordinator, Presentable {
private let parameters: InfoSheetCoordinatorParameters
private let infoSheetHostingController: UIViewController
private var infoSheetViewModel: InfoSheetViewModelProtocol

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

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

let viewModel = InfoSheetViewModel(title: parameters.title, description: parameters.description, action: parameters.action)
let view = InfoSheet(viewModel: viewModel.context)
infoSheetViewModel = viewModel
let controller = VectorHostingController(rootView: view)
infoSheetHostingController = controller
setupPresentation(of: controller)
}

// MARK: - Public

func start() {
MXLog.debug("[InfoSheetCoordinator] did start.")
infoSheetViewModel.completion = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[InfoSheetCoordinator] InfoSheetViewModel did complete with result: \(result).")
self.completion?(result)
}
}

func toPresentable() -> UIViewController {
infoSheetHostingController
}
}

private extension InfoSheetCoordinator {
// The bottom sheet should be presented with the content intrinsic height as for design requirement
// We can do it easily just on iOS 16+
func setupPresentation(of viewController: VectorHostingController) {
let cornerRadius: CGFloat = 24

guard
#available(iOS 16, *),
let parentSize = parameters.parentSize,
let presentationController = viewController.sheetPresentationController
else {
viewController.bottomSheetPreferences = .init(cornerRadius: cornerRadius)
return
}

let intrisincSize = viewController.view.systemLayoutSizeFitting(.init(width: parentSize.width, height: 0),
withHorizontalFittingPriority: .defaultHigh,
verticalFittingPriority: .defaultLow)

presentationController.preferredCornerRadius = cornerRadius
presentationController.prefersGrabberVisible = true
presentationController.detents = [
.custom { context in
min(context.maximumDetentValue, intrisincSize.height)
},
.large()
]
}
}
33 changes: 33 additions & 0 deletions RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// 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.
//

// MARK: View model

enum InfoSheetViewModelResult {
case actionTriggered
}

// MARK: View

struct InfoSheetViewState: BindableState {
let title: String
let description: String
let action: InfoSheet.Action
}

enum InfoSheetViewAction {
case actionTriggered
}
36 changes: 36 additions & 0 deletions RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// 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

typealias InfoSheetViewModelType = StateStoreViewModel<InfoSheetViewState, InfoSheetViewAction>

class InfoSheetViewModel: InfoSheetViewModelType, InfoSheetViewModelProtocol {
var completion: ((InfoSheetViewModelResult) -> Void)?

init(title: String, description: String, action: InfoSheet.Action) {
super.init(initialViewState: InfoSheetViewState(title: title, description: description, action: action))
}

// MARK: - Public

override func process(viewAction: InfoSheetViewAction) {
switch viewAction {
case .actionTriggered:
completion?(.actionTriggered)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// 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 InfoSheetViewModelProtocol {
var completion: ((InfoSheetViewModelResult) -> Void)? { get set }
var context: InfoSheetViewModelType.Context { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import 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 MockInfoSheetScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case sheet(title: String, subtitle: String, action: InfoSheet.Action)

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

/// A list of screen state definitions
static var allCases: [MockInfoSheetScreenState] {
// Each of the presence statuses
[.sheet(title: VectorL10n.userSessionVerifiedSessionTitle, subtitle: VectorL10n.userSessionVerifiedSessionDescription, action: .init(text: VectorL10n.userSessionGotIt, action: { }))]
}

/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let model: (title: String, subtitle: String, action: InfoSheet.Action)

switch self {
case let .sheet(title, subtitle, action):
model = (title, subtitle, action)
}
let viewModel = InfoSheetViewModel(title: model.title, description: model.subtitle, action: model.action)

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

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