From de029b61daf7c855bbb3bc4439acbe21169d5b87 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 10:34:10 +0200 Subject: [PATCH 01/35] Device manager: Add strings. --- Riot/Assets/en.lproj/Vector.strings | 13 ++++++++++ Riot/Generated/Strings.swift | 40 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index f642e3993..948406fff 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2357,6 +2357,19 @@ To enable access, tap Settings> Location and select Always"; "user_sessions_overview_title" = "Sessions"; +"user_session_verified" = "Verified session"; +"user_session_unverified" = "Unverified session"; +"user_session_verified_short" = "Verified"; +"user_session_unverified_short" = "Unverified"; + +"user_session_name" = "%@: %@"; +"user_session_item_details" = "%@ · Last activity %@"; + +"device_name_desktop" = "%@ Desktop"; +"device_name_web" = "%@ Web"; +"device_name_mobile" = "%@ Mobile"; +"device_name_unknown" = "Unknown client"; + // MARK: - MatrixKit diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 1a079a716..5dad3f8e1 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1451,6 +1451,22 @@ public class VectorL10n: NSObject { public static var deviceDetailsTitle: String { return VectorL10n.tr("Vector", "device_details_title") } + /// %@ Desktop + public static func deviceNameDesktop(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_name_desktop", p1) + } + /// %@ Mobile + public static func deviceNameMobile(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_name_mobile", p1) + } + /// Unknown client + public static var deviceNameUnknown: String { + return VectorL10n.tr("Vector", "device_name_unknown") + } + /// %@ Web + public static func deviceNameWeb(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_name_web", p1) + } /// The other party cancelled the verification. public static var deviceVerificationCancelled: String { return VectorL10n.tr("Vector", "device_verification_cancelled") @@ -8447,6 +8463,30 @@ public class VectorL10n: NSObject { public static var userIdTitle: String { return VectorL10n.tr("Vector", "user_id_title") } + /// %@ · Last activity %@ + public static func userSessionItemDetails(_ p1: String, _ p2: String) -> String { + return VectorL10n.tr("Vector", "user_session_item_details", p1, p2) + } + /// %@: %@ + public static func userSessionName(_ p1: String, _ p2: String) -> String { + return VectorL10n.tr("Vector", "user_session_name", p1, p2) + } + /// Unverified session + public static var userSessionUnverified: String { + return VectorL10n.tr("Vector", "user_session_unverified") + } + /// Unverified + public static var userSessionUnverifiedShort: String { + return VectorL10n.tr("Vector", "user_session_unverified_short") + } + /// Verified session + public static var userSessionVerified: String { + return VectorL10n.tr("Vector", "user_session_verified") + } + /// Verified + public static var userSessionVerifiedShort: String { + return VectorL10n.tr("Vector", "user_session_verified_short") + } /// Sessions public static var userSessionsOverviewTitle: String { return VectorL10n.tr("Vector", "user_sessions_overview_title") From 6857ccdfcf4468f317ed3614b4a8ffb280297894 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 10:35:13 +0200 Subject: [PATCH 02/35] Device manager: Add assets. --- .../Images.xcassets/DeviceManager/Contents.json | 6 ++++++ .../device_type_desktop.imageset/Contents.json | 12 ++++++++++++ .../device_type_desktop.svg | 3 +++ .../device_type_mobile.imageset/Contents.json | 12 ++++++++++++ .../device_type_mobile.svg | 3 +++ .../device_type_unknown.imageset/Contents.json | 12 ++++++++++++ .../device_type_unknown.svg | 3 +++ .../device_type_web.imageset/Contents.json | 12 ++++++++++++ .../device_type_web.imageset/device_type_web.svg | 3 +++ .../user_session_unverified.imageset/Contents.json | 12 ++++++++++++ .../user_session_unverified.svg | 4 ++++ .../user_session_verified.imageset/Contents.json | 12 ++++++++++++ .../user_session_verified.svg | 4 ++++ Riot/Generated/Images.swift | 9 +++++++++ 14 files changed, 107 insertions(+) create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/device_type_desktop.svg create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/device_type_mobile.svg create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/device_type_unknown.svg create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/device_type_web.svg create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/user_session_unverified.svg create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/user_session_verified.svg diff --git a/Riot/Assets/Images.xcassets/DeviceManager/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/Contents.json new file mode 100644 index 000000000..7037289bf --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "device_type_desktop.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/device_type_desktop.svg b/Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/device_type_desktop.svg new file mode 100644 index 000000000..de88604bc --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_desktop.imageset/device_type_desktop.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/Contents.json new file mode 100644 index 000000000..edaabf82f --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "device_type_mobile.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/device_type_mobile.svg b/Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/device_type_mobile.svg new file mode 100644 index 000000000..f34e3dba9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_mobile.imageset/device_type_mobile.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/Contents.json new file mode 100644 index 000000000..1d57255bc --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "device_type_unknown.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/device_type_unknown.svg b/Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/device_type_unknown.svg new file mode 100644 index 000000000..8a7b7f1ba --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_unknown.imageset/device_type_unknown.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/Contents.json new file mode 100644 index 000000000..30d46e64e --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "device_type_web.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/device_type_web.svg b/Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/device_type_web.svg new file mode 100644 index 000000000..34424d375 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/device_type_web.imageset/device_type_web.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/Contents.json new file mode 100644 index 000000000..363025375 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_session_unverified.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/user_session_unverified.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/user_session_unverified.svg new file mode 100644 index 000000000..ee304f46e --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_unverified.imageset/user_session_unverified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/Contents.json new file mode 100644 index 000000000..cf9c2286a --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_session_verified.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/user_session_verified.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/user_session_verified.svg new file mode 100644 index 000000000..cc3459fd2 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_verified.imageset/user_session_verified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 4aaf0337d..7839c3804 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -101,6 +101,12 @@ internal class Asset: NSObject { internal static let findYourContactsFacepile = ImageAsset(name: "find_your_contacts_facepile") internal static let captureAvatar = ImageAsset(name: "capture_avatar") internal static let deleteAvatar = ImageAsset(name: "delete_avatar") + internal static let deviceTypeDesktop = ImageAsset(name: "device_type_desktop") + internal static let deviceTypeMobile = ImageAsset(name: "device_type_mobile") + internal static let deviceTypeUnknown = ImageAsset(name: "device_type_unknown") + internal static let deviceTypeWeb = ImageAsset(name: "device_type_web") + internal static let userSessionUnverified = ImageAsset(name: "user_session_unverified") + internal static let userSessionVerified = ImageAsset(name: "user_session_verified") internal static let e2eBlocked = ImageAsset(name: "e2e_blocked") internal static let e2eUnencrypted = ImageAsset(name: "e2e_unencrypted") internal static let e2eWarning = ImageAsset(name: "e2e_warning") @@ -109,6 +115,8 @@ internal class Asset: NSObject { internal static let encryptionWarning = ImageAsset(name: "encryption_warning") internal static let favouritesEmptyScreenArtwork = ImageAsset(name: "favourites_empty_screen_artwork") internal static let favouritesEmptyScreenArtworkDark = ImageAsset(name: "favourites_empty_screen_artwork_dark") + internal static let allChatRecents = ImageAsset(name: "all_chat_recents") + internal static let allChatUnreads = ImageAsset(name: "all_chat_unreads") internal static let roomActionDirectChat = ImageAsset(name: "room_action_direct_chat") internal static let roomActionFavourite = ImageAsset(name: "room_action_favourite") internal static let roomActionLeave = ImageAsset(name: "room_action_leave") @@ -116,6 +124,7 @@ internal class Asset: NSObject { internal static let roomActionNotificationMuted = ImageAsset(name: "room_action_notification_muted") internal static let roomActionPriorityHigh = ImageAsset(name: "room_action_priority_high") internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low") + internal static let allChatEditLayout = ImageAsset(name: "all_chat_edit_layout") internal static let allChatsEditIcon = ImageAsset(name: "all_chats_edit_icon") internal static let allChatsEmptyListPlaceholderIcon = ImageAsset(name: "all_chats_empty_list_placeholder_icon") internal static let allChatsSpacesIcon = ImageAsset(name: "all_chats_spaces_icon") From f07083fd3ccf41f9fbd6d16ea9e4cab5664f4b06 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 10:37:47 +0200 Subject: [PATCH 03/35] Create DeviceType enum. --- .../DeviceType/DeviceType+Element.swift | 58 +++++++++++++++++++ .../UserSessions/DeviceType/DeviceType.swift | 26 +++++++++ 2 files changed, 84 insertions(+) create mode 100644 RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType+Element.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType.swift diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType+Element.swift b/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType+Element.swift new file mode 100644 index 000000000..ecbad3537 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType+Element.swift @@ -0,0 +1,58 @@ +// +// 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 + +extension DeviceType { + + var image: Image { + + let image: Image + + switch self { + case .desktop: + image = Image(Asset.Images.deviceTypeDesktop.name) + case .web: + image = Image(Asset.Images.deviceTypeWeb.name) + case .mobile: + image = Image(Asset.Images.deviceTypeMobile.name) + case .unknown: + image = Image(Asset.Images.deviceTypeUnknown.name) + } + + return image + } + + var name: String { + let name: String + + let appName = AppInfo.current.displayName + + switch self { + case .desktop: + name = VectorL10n.deviceNameDesktop(appName) + case .web: + name = VectorL10n.deviceNameWeb(appName) + case .mobile: + name = VectorL10n.deviceNameMobile(appName) + case .unknown: + name = VectorL10n.deviceNameUnknown + } + + return name + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType.swift b/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType.swift new file mode 100644 index 000000000..1ae053b1a --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType.swift @@ -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 Foundation +import SwiftUI + +/// Client type +enum DeviceType { + case desktop + case web + case mobile + case unknown +} From cdbf773cdebe92835bad74c81efd60d2033309ad Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 10:40:03 +0200 Subject: [PATCH 04/35] Create DeviceAvatarView. --- .../DeviceAvatar/DeviceAvatarView.swift | 97 +++++++++++++++++++ .../DeviceAvatar/DeviceAvatarViewData.swift | 26 +++++ 2 files changed, 123 insertions(+) create mode 100644 RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarViewData.swift diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift new file mode 100644 index 000000000..9c54d2f40 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift @@ -0,0 +1,97 @@ +// +// 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 DesignKit + +/// Avatar view for device +struct DeviceAvatarView: View { + + @Environment(\.theme) var theme: ThemeSwiftUI + + var viewData: DeviceAvatarViewData + + var avatarSize: Float = 40.0 + var badgeSize: Float { + return 24 + } + + var body: some View { + ZStack { + VStack { + VStack(alignment: .center) { + viewData.deviceType.image + } + .padding() + } + .frame(maxWidth: CGFloat(avatarSize), maxHeight: CGFloat(avatarSize)) + .background(theme.colors.system) + .clipShape(Circle()) + + if let isVerified = viewData.isVerified { + + VStack(alignment: .trailing) { + Spacer() + HStack(alignment: .bottom) { + Spacer() + VStack() { + Image(isVerified ? Asset.Images.userSessionVerified.name : Asset.Images.userSessionUnverified.name) + } + .frame(maxWidth: CGFloat(badgeSize), maxHeight: CGFloat(badgeSize)) + .shapedBorder(color: theme.colors.system, borderWidth: 1, shape: Circle()) + .background(theme.colors.background) + .clipShape(Circle()) + } + .offset(x: 10, y: 8) + } + } + } + .frame(maxWidth: CGFloat(avatarSize), maxHeight: CGFloat(avatarSize)) + } +} + +struct DeviceAvatarViewListPreview: View { + + var viewDataList: [DeviceAvatarViewData] { + return [ + DeviceAvatarViewData(deviceType: .desktop, isVerified: true), + DeviceAvatarViewData(deviceType: .web, isVerified: true), + DeviceAvatarViewData(deviceType: .mobile, isVerified: true), + DeviceAvatarViewData(deviceType: .unknown, isVerified: true) + ] + } + + var body: some View { + HStack { + VStack(alignment: .center, spacing: 20) { + DeviceAvatarView(viewData: DeviceAvatarViewData.init(deviceType: .web, isVerified: true)) + DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .desktop, isVerified: false)) + DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .mobile, isVerified: true)) + DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .unknown, isVerified: false)) + } + } + } +} + +struct DeviceAvatarView_Previews: PreviewProvider { + + static var previews: some View { + Group { + DeviceAvatarViewListPreview().theme(.light).preferredColorScheme(.light) + DeviceAvatarViewListPreview().theme(.dark).preferredColorScheme(.dark) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarViewData.swift b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarViewData.swift new file mode 100644 index 000000000..19c6cfd1d --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarViewData.swift @@ -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 Foundation +import SwiftUI + +/// View data for DeviceAvatarView +struct DeviceAvatarViewData { + + let deviceType: DeviceType + + let isVerified: Bool? +} From 1130c65cf681bce91758e5ea4c6c12e357f01f05 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 10:41:35 +0200 Subject: [PATCH 05/35] Create UserSessionInfo that represents a user session information. --- .../UserSessionInfo/UserSessionInfo.swift | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 RiotSwiftUI/Modules/UserSessions/UserSessionInfo/UserSessionInfo.swift diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionInfo/UserSessionInfo.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionInfo/UserSessionInfo.swift new file mode 100644 index 000000000..6009bfcb2 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionInfo/UserSessionInfo.swift @@ -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 Foundation + +/// Represents a user session information +struct UserSessionInfo: Identifiable { + + /// Delay after which session is considered inactive, 90 days + static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400 + + // MARK: - Properties + + var id: String { + return sessionId + } + + /// The session identifier + let sessionId: String + + /// The session display name + let sessionName: String? + + /// The device type used by the session + let deviceType: DeviceType + + /// True to indicate that the session is verified + let isVerified: Bool + + /// The IP address where this device was last seen. + let lastSeenIP: String? + + /// Last time the session was active + let lastSeenTimestamp: TimeInterval? + + /// True to indicate that session has been used under `inactiveSessionDurationTreshold` value + let isSessionActive: Bool + + // MARK: - Setup + + init(sessionId: String, + sessionName: String?, + deviceType: DeviceType, + isVerified: Bool, + lastSeenIP: String?, + lastSeenTimestamp: TimeInterval?) { + + self.sessionId = sessionId + self.sessionName = sessionName + self.deviceType = deviceType + self.isVerified = isVerified + self.lastSeenIP = lastSeenIP + self.lastSeenTimestamp = lastSeenTimestamp + + if let lastSeenTimestamp = lastSeenTimestamp { + let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp + + let isSessionInactive = elapsedTime >= Self.inactiveSessionDurationTreshold + + self.isSessionActive = !isSessionInactive + } else { + self.isSessionActive = true + } + } +} From 5f58b8eb230ee81bc5906796865bb4ff101d3dee Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 10:44:18 +0200 Subject: [PATCH 06/35] Create UserSessionListItem view. --- .../View/UserSessionListItem.swift | 136 ++++++++++++++++++ .../View/UserSessionListItemViewData.swift | 61 ++++++++ 2 files changed, 197 insertions(+) create mode 100644 RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift new file mode 100644 index 000000000..1c2f18cdd --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -0,0 +1,136 @@ +// +// 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 UserSessionListItem: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + private var sessionTitle: String { + + let sessionTitle: String + + let clientName = viewData.deviceType.name + + if let sessionName = viewData.sessionName { + sessionTitle = VectorL10n.userSessionName(clientName, sessionName) + } else { + sessionTitle = clientName + } + + return sessionTitle + } + + private var sessionDetailsText: String { + + let sessionDetailsString: String + + let sessionStatusText = viewData.isVerified ? VectorL10n.userSessionVerifiedShort : VectorL10n.userSessionUnverifiedShort + + var lastActivityDateString: String? + + if let lastActivityDate = viewData.lastActivityDate { + lastActivityDateString = self.lastActivityDateString(from: lastActivityDate) + } + + if let lastActivityDateString = lastActivityDateString { + sessionDetailsString = VectorL10n.userSessionItemDetails(sessionStatusText, lastActivityDateString) + } else { + sessionDetailsString = sessionStatusText + } + + return sessionDetailsString + } + + // MARK: Public + + let viewData: UserSessionListItemViewData + + var onBackgroundTap: ((String) -> (Void))? = nil + + // MARK: - Body + + var body: some View { + HStack { + HStack(spacing: 18) { + DeviceAvatarView(viewData: viewData.deviceAvatarViewData) + VStack(alignment: .leading, spacing: 2) { Text(sessionTitle) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + Text(sessionDetailsText) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 15) + .onTapGesture { + onBackgroundTap?(self.viewData.sessionId) + } + } + + // MARK: - Private + + private func lastActivityDateString(from timestamp: TimeInterval) -> String? { + + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + + let date = Date(timeIntervalSince1970: timestamp) + + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.doesRelativeDateFormatting = true + + let dateString = dateFormatter.string(from: date) + + return dateString + } +} + +struct UserSessionListPreview: View { + + let userSessionsOverviewService: UserSessionsOverviewServiceProtocol = MockUserSessionsOverviewService() + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + ForEach(userSessionsOverviewService.lastOverviewData.otherSessionsInfo) { userSessionInfo in + let viewData = UserSessionListItemViewData(userSessionInfo: userSessionInfo) + + UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + + }) + } + Spacer() + } + .padding() + } +} + +struct UserSessionListItem_Previews: PreviewProvider { + static var previews: some View { + Group { + UserSessionListPreview().theme(.light).preferredColorScheme(.light) + UserSessionListPreview().theme(.dark).preferredColorScheme(.dark) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift new file mode 100644 index 000000000..c3c699789 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift @@ -0,0 +1,61 @@ +// +// 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 + +/// View data for UserSessionListItem +struct UserSessionListItemViewData: Identifiable { + + // MARK: - Properties + + var id: String { + return sessionId + } + + let sessionId: String + + let sessionName: String? + + let deviceType: DeviceType + + let isVerified: Bool + + let lastActivityDate: TimeInterval? + + let deviceAvatarViewData: DeviceAvatarViewData + + // MARK: - Setup + + init(sessionId: String, + sessionName: String?, + deviceType: DeviceType, + isVerified: Bool, + lastActivityDate: TimeInterval?) { + self.sessionId = sessionId + self.sessionName = sessionName + self.deviceType = deviceType + self.isVerified = isVerified + self.lastActivityDate = lastActivityDate + self.deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: isVerified) + } +} + +extension UserSessionListItemViewData { + + init(userSessionInfo: UserSessionInfo) { + self.init(sessionId: userSessionInfo.sessionId, sessionName: userSessionInfo.sessionName, deviceType: userSessionInfo.deviceType, isVerified: userSessionInfo.isVerified, lastActivityDate: userSessionInfo.lastSeenTimestamp) + } +} From 069f0f3684b3a3e31f501e875c11eb3f0fdeb177 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 10:49:37 +0200 Subject: [PATCH 07/35] Implement UserSessionsOverviewService. --- .../UserSessionsOverviewService.swift | 109 +++++++++++++++++- .../MockUserSessionsOverviewService.swift | 22 ++++ .../UserSessionsOverviewServiceProtocol.swift | 14 ++- 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift index 1f280ad1a..bccb7e9f7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift @@ -15,11 +15,118 @@ // import Foundation +import MatrixSDK class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol { + // MARK: - Constants + + // MARK: - Properties + + // MARK: Private + + private let mxSession: MXSession + + // MARK: Public + + private(set) var lastOverviewData: UserSessionsOverviewData + // MARK: - Setup - init() { + init(mxSession: MXSession) { + self.mxSession = mxSession + + self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: nil, unverifiedSessionsInfo: [], inactiveSessionsInfo: [], otherSessionsInfo: []) + + self.setupInitialOverviewData() + } + + // MARK: - Public + + func fetchUserSessionsOverviewData(completion: @escaping (Result) -> Void) { + self.mxSession.matrixRestClient.devices { response in + switch response { + case .success(let devices): + let overviewData = self.userSessionsOverviewData(from: devices) + completion(.success(overviewData)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: - Private + + private func setupInitialOverviewData() { + let currentSessionInfo = self.getCurrentUserSessionInfoFromCache() + + self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: [], inactiveSessionsInfo: [], otherSessionsInfo: []) + } + + private func getCurrentUserSessionInfoFromCache() -> UserSessionInfo? { + guard let mainAccount = MXKAccountManager.shared().activeAccounts.first, let device = mainAccount.device else { + return nil + } + return self.userSessionInfo(from: device) + } + + private func userSessionInfo(from device: MXDevice) -> UserSessionInfo { + + let deviceInfo = self.getDeviceInfo(for: device.deviceId) + + let isSessionVerified = deviceInfo?.trustLevel.isVerified ?? false + let lastSeenTs = TimeInterval(device.lastSeenTs * 1000) + + return UserSessionInfo(sessionId: device.deviceId, + sessionName: device.displayName, + deviceType: .unknown, + isVerified: isSessionVerified, + lastSeenIP: device.lastSeenIp, + lastSeenTimestamp: lastSeenTs) + } + + private func getDeviceInfo(for deviceId: String) -> MXDeviceInfo? { + guard let userId = self.mxSession.myUserId else { + return nil + } + return self.mxSession.crypto.device(withDeviceId: deviceId, ofUser: userId) + } + + private func userSessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData { + + let sortedDevices = devices.sorted { device1, device2 in + device1.lastSeenTs > device2.lastSeenTs + } + + let allUserSessionInfo = sortedDevices.map { device in + return self.userSessionInfo(from: device) + } + + var currentSessionInfo: UserSessionInfo? + + var unverifiedSessionsInfo: [UserSessionInfo] = [] + var inactiveSessionsInfo: [UserSessionInfo] = [] + var otherSessionsInfo: [UserSessionInfo] = [] + + for userSessionInfo in allUserSessionInfo { + if userSessionInfo.sessionId == self.mxSession.myDeviceId { + currentSessionInfo = userSessionInfo + } else { + otherSessionsInfo.append(userSessionInfo) + + if userSessionInfo.isVerified == false { + unverifiedSessionsInfo.append(userSessionInfo) + } + + if userSessionInfo.isSessionActive == false { + inactiveSessionsInfo.append(userSessionInfo) + } + } + } + + return UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, + unverifiedSessionsInfo: unverifiedSessionsInfo, + inactiveSessionsInfo: inactiveSessionsInfo, + otherSessionsInfo: otherSessionsInfo) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift index 61037fec8..c4cc729e1 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift @@ -17,4 +17,26 @@ import Foundation class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { + + var lastOverviewData: UserSessionsOverviewData + + func fetchUserSessionsOverviewData(completion: @escaping (Result) -> Void) { + completion(.success(self.lastOverviewData)) + } + + init() { + let currentSessionInfo = UserSessionInfo(sessionId: "alice", sessionName: "iOS", deviceType: .mobile, isVerified: false, lastSeenIP: "10.0.0.10", lastSeenTimestamp: nil) + + let unverifiedSessionsInfo: [UserSessionInfo] = [] + + let inactiveSessionsInfo: [UserSessionInfo] = [] + + let otherSessionsInfo: [UserSessionInfo] = [ + UserSessionInfo(sessionId: "1", sessionName: "macOS", deviceType: .desktop, isVerified: true, lastSeenIP: "1.0.0.1", lastSeenTimestamp: (Date().timeIntervalSince1970 - 130000)), + UserSessionInfo(sessionId: "2", sessionName: "Firefox on Windows", deviceType: .web, isVerified: true, lastSeenIP: "2.0.0.2", lastSeenTimestamp: (Date().timeIntervalSince1970 - 100)), + UserSessionInfo(sessionId: "3", sessionName: "Android", deviceType: .mobile, isVerified: false, lastSeenIP: "3.0.0.3", lastSeenTimestamp: (Date().timeIntervalSince1970 - 10)) + ] + + self.lastOverviewData = UserSessionsOverviewData.init(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: unverifiedSessionsInfo, inactiveSessionsInfo: inactiveSessionsInfo, otherSessionsInfo: otherSessionsInfo) + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift index be124d126..e7774be69 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift @@ -16,5 +16,17 @@ import Foundation -protocol UserSessionsOverviewServiceProtocol { +struct UserSessionsOverviewData { + + let currentSessionInfo: UserSessionInfo? + let unverifiedSessionsInfo: [UserSessionInfo] + let inactiveSessionsInfo: [UserSessionInfo] + let otherSessionsInfo: [UserSessionInfo] +} + +protocol UserSessionsOverviewServiceProtocol { + + var lastOverviewData: UserSessionsOverviewData { get } + + func fetchUserSessionsOverviewData(completion: @escaping (Result) -> Void) -> Void } From 33a93c9e6e407af825101eca29e66978091eb330 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 11:14:40 +0200 Subject: [PATCH 08/35] UserSessionsOverviewService: Fix UserSessionInfo last seen ts building. --- .../Service/MatrixSDK/UserSessionsOverviewService.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift index bccb7e9f7..e3670da92 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift @@ -75,7 +75,12 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol { let deviceInfo = self.getDeviceInfo(for: device.deviceId) let isSessionVerified = deviceInfo?.trustLevel.isVerified ?? false - let lastSeenTs = TimeInterval(device.lastSeenTs * 1000) + + var lastSeenTs: TimeInterval? + + if device.lastSeenTs > 0 { + lastSeenTs = TimeInterval(device.lastSeenTs / 1000) + } return UserSessionInfo(sessionId: device.deviceId, sessionName: device.displayName, From d87fc2dc73adcab40b1bf5df7ca06e1beb2420ea Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 11:15:37 +0200 Subject: [PATCH 09/35] Create UserSessionsOverview screen. --- .../UserSessionsOverviewCoordinator.swift | 71 ++++++++++++++++++- .../UserSessionsOverviewModels.swift | 21 +++++- .../UserSessionsOverviewViewModel.swift | 67 +++++++++++++++-- .../View/UserSessionListItem.swift | 2 +- .../View/UserSessionsOverview.swift | 29 +++++++- 5 files changed, 178 insertions(+), 12 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index 5abebc3ab..1220de7c3 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -31,6 +31,9 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { private let userSessionsOverviewHostingController: UIViewController private var userSessionsOverviewViewModel: UserSessionsOverviewViewModelProtocol + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + // MARK: Public // Must be used only internally @@ -41,10 +44,15 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { init(parameters: UserSessionsOverviewCoordinatorParameters) { self.parameters = parameters - let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: UserSessionsOverviewService()) + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: UserSessionsOverviewService(mxSession: parameters.session)) let view = UserSessionsOverview(viewModel: viewModel.context) userSessionsOverviewViewModel = viewModel - userSessionsOverviewHostingController = VectorHostingController(rootView: view) + + let hostingViewController = VectorHostingController(rootView: view) + + userSessionsOverviewHostingController = hostingViewController + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController) } // MARK: - Public @@ -55,8 +63,22 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { guard let self = self else { return } MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).") switch result { - case .done: + case .cancel: self.completion?() + case .loadData: + self.loadData() + case .showAllUnverifiedSessions: + self.showAllUnverifiedSessions() + case .showAllInactiveSessions: + self.showAllInactiveSessions() + case .verifyCurrentSession: + self.startVerifyCurrentSession() + case .showCurrentSessionDetails: + self.showCurrentSessionDetails() + case .showAllOtherSessions: + self.showAllOtherSessions() + case .showUserSessionDetails(let sessionId): + self.showUserSessionDetails(sessionId: sessionId) } } } @@ -64,4 +86,47 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { func toPresentable() -> UIViewController { return self.userSessionsOverviewHostingController } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } + + private func loadData() { + // TODO + } + + private func showAllUnverifiedSessions() { + // TODO + } + + private func showAllInactiveSessions() { + // TODO + } + + private func startVerifyCurrentSession() { + // TODO + } + + private func showCurrentSessionDetails() { + // TODO + } + + private func showUserSessionDetails(sessionId: String) { + // TODO + } + + private func showAllOtherSessions() { + // TODO + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift index 949617ca6..438028b5f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -21,18 +21,37 @@ import Foundation // MARK: View model enum UserSessionsOverviewViewModelResult { - case done + case cancel + case loadData + case showAllUnverifiedSessions + case showAllInactiveSessions + case verifyCurrentSession + case showCurrentSessionDetails + case showAllOtherSessions + case showUserSessionDetails(_ sessionId: String) } // MARK: View struct UserSessionsOverviewViewState: BindableState { + + var unverifiedSessionsViewData: [UserSessionListItemViewData] + + var inactiveSessionsViewData: [UserSessionListItemViewData] + + var currentSessionViewData: UserSessionListItemViewData? + + var otherSessionsViewData: [UserSessionListItemViewData] + + var showLoadingIndicator: Bool = false } enum UserSessionsOverviewViewAction { + case viewAppeared case verifyCurrentSession case viewCurrentSessionDetails case viewAllUnverifiedSessions case viewAllInactiveSessions case viewAllOtherSessions + case tapUserSession(_ sessionId: String) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index 93c28688c..9e000bb94 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -37,25 +37,80 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess init(userSessionsOverviewService: UserSessionsOverviewServiceProtocol) { self.userSessionsOverviewService = userSessionsOverviewService - let viewState = UserSessionsOverviewViewState() + let viewState = UserSessionsOverviewViewState(unverifiedSessionsViewData: [], inactiveSessionsViewData: [], currentSessionViewData: nil, otherSessionsViewData: []) super.init(initialViewState: viewState) + + self.updateViewState(with: userSessionsOverviewService.lastOverviewData) } // MARK: - Public override func process(viewAction: UserSessionsOverviewViewAction) { switch viewAction { + case .viewAppeared: + self.loadData() case .verifyCurrentSession: - break + self.completion?(.verifyCurrentSession) case .viewCurrentSessionDetails: - break + self.completion?(.showCurrentSessionDetails) case .viewAllUnverifiedSessions: - break + self.completion?(.showAllUnverifiedSessions) case .viewAllInactiveSessions: - break + self.completion?(.showAllInactiveSessions) case .viewAllOtherSessions: - break + self.completion?(.showAllOtherSessions) + case .tapUserSession(let sessionId): + self.completion?(.showUserSessionDetails(sessionId)) + } + } + + // MARK: - Private + + private func updateViewState(with userSessionsViewData: UserSessionsOverviewData) { + + let unverifiedSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.unverifiedSessionsInfo) + let inactiveSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.inactiveSessionsInfo) + + var currentSessionViewData: UserSessionListItemViewData? + + let otherSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.otherSessionsInfo) + + + if let currentSessionInfo = userSessionsViewData.currentSessionInfo { + currentSessionViewData = UserSessionListItemViewData(userSessionInfo: currentSessionInfo) + } + + self.state.unverifiedSessionsViewData = unverifiedSessionsViewData + self.state.inactiveSessionsViewData = inactiveSessionsViewData + self.state.currentSessionViewData = currentSessionViewData + self.state.otherSessionsViewData = otherSessionsViewData + } + + private func userSessionListItemViewDataList(from userSessionInfoList: [UserSessionInfo]) -> [UserSessionListItemViewData] { + return userSessionInfoList.map { + return UserSessionListItemViewData(userSessionInfo: $0) + } + } + + private func loadData() { + + self.state.showLoadingIndicator = true + + self.userSessionsOverviewService.fetchUserSessionsOverviewData { [weak self] result in + guard let self = self else { + return + } + + self.state.showLoadingIndicator = false + + switch result { + case .success(let overViewData): + self.updateViewState(with: overViewData) + case .failure(let error): + // TODO + break + } } } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 1c2f18cdd..ca91a6f26 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -51,7 +51,7 @@ struct UserSessionListItem: View { lastActivityDateString = self.lastActivityDateString(from: lastActivityDate) } - if let lastActivityDateString = lastActivityDateString { + if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false { sessionDetailsString = VectorL10n.userSessionItemDetails(sessionStatusText, lastActivityDateString) } else { sessionDetailsString = sessionStatusText diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 1dc2983fb..9bb0512d8 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -29,11 +29,38 @@ struct UserSessionsOverview: View { @ObservedObject var viewModel: UserSessionsOverviewViewModel.Context var body: some View { - VStack { + ScrollView { + + // Security recommendations section + if viewModel.viewState.unverifiedSessionsViewData.isEmpty == false || viewModel.viewState.inactiveSessionsViewData.isEmpty == false { + + // TODO: + } + + // Current session section + if let currentSessionViewData = viewModel.viewState.currentSessionViewData { + // TODO: + } + + // Other sessions section + if viewModel.viewState.otherSessionsViewData.isEmpty == false { + + VStack(spacing: 15) { + ForEach(viewModel.viewState.otherSessionsViewData) { viewData in + UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + viewModel.send(viewAction: .tapUserSession(sessionId)) + }) + } + } + } } .background(theme.colors.background) .frame(maxHeight: .infinity) .navigationTitle(VectorL10n.userSessionsOverviewTitle) + .activityIndicator(show: viewModel.viewState.showLoadingIndicator) + .onAppear() { + viewModel.send(viewAction: .viewAppeared) + } } } From 7f11c52ad074a4db6450e11bfbdc8571756eabda Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 12:08:27 +0200 Subject: [PATCH 10/35] User sessions overview: Add strings. --- Riot/Assets/en.lproj/Vector.strings | 3 +++ Riot/Generated/Strings.swift | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 948406fff..f2dd394c9 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2357,6 +2357,9 @@ To enable access, tap Settings> Location and select Always"; "user_sessions_overview_title" = "Sessions"; +"user_sessions_overview_other_sessions_section_title" = "OTHER SESSIONS"; +"user_sessions_overview_other_sessions_section_info" = "For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore."; + "user_session_verified" = "Verified session"; "user_session_unverified" = "Unverified session"; "user_session_verified_short" = "Verified"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 5dad3f8e1..a1cff2734 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8487,6 +8487,14 @@ public class VectorL10n: NSObject { public static var userSessionVerifiedShort: String { return VectorL10n.tr("Vector", "user_session_verified_short") } + /// For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. + public static var userSessionsOverviewOtherSessionsSectionInfo: String { + return VectorL10n.tr("Vector", "user_sessions_overview_other_sessions_section_info") + } + /// OTHER SESSIONS + public static var userSessionsOverviewOtherSessionsSectionTitle: String { + return VectorL10n.tr("Vector", "user_sessions_overview_other_sessions_section_title") + } /// Sessions public static var userSessionsOverviewTitle: String { return VectorL10n.tr("Vector", "user_sessions_overview_title") From 98c272a86c2ee9ed10c2f6cc66cde39eed64e824 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 5 Sep 2022 12:09:26 +0200 Subject: [PATCH 11/35] UserSessionsOverview: Add text headers to other sessions section. --- .../View/UserSessionListItem.swift | 4 +-- .../View/UserSessionsOverview.swift | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index ca91a6f26..701d0ca84 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -101,9 +101,7 @@ struct UserSessionListItem: View { dateFormatter.timeStyle = .short dateFormatter.doesRelativeDateFormatting = true - let dateString = dateFormatter.string(from: date) - - return dateString + return dateFormatter.string(from: date) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 9bb0512d8..2186ad241 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -45,16 +45,36 @@ struct UserSessionsOverview: View { // Other sessions section if viewModel.viewState.otherSessionsViewData.isEmpty == false { - VStack(spacing: 15) { - ForEach(viewModel.viewState.otherSessionsViewData) { viewData in - UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in - viewModel.send(viewAction: .tapUserSession(sessionId)) - }) + VStack() { + + // Section header + VStack(alignment: .leading) { + Text(VectorL10n.userSessionsOverviewOtherSessionsSectionTitle) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 10) + + Text(VectorL10n.userSessionsOverviewOtherSessionsSectionInfo) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 11) } + .padding(.horizontal, 16) + + // Device list + VStack(spacing: 16) { + ForEach(viewModel.viewState.otherSessionsViewData) { viewData in + UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + viewModel.send(viewAction: .tapUserSession(sessionId)) + }) + } + } + .padding(.vertical, 16) + .background(theme.colors.background) } } } - .background(theme.colors.background) + .background(theme.colors.system) .frame(maxHeight: .infinity) .navigationTitle(VectorL10n.userSessionsOverviewTitle) .activityIndicator(show: viewModel.viewState.showLoadingIndicator) From 56d9797703ea3dc0702986f1521bacb62047db0e Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 6 Sep 2022 16:44:34 +0200 Subject: [PATCH 12/35] UserSessionsOverview: Fix safe area background color. --- .../UserSessionsOverview/View/UserSessionsOverview.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 2186ad241..9a1719767 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -74,7 +74,7 @@ struct UserSessionsOverview: View { } } } - .background(theme.colors.system) + .background(theme.colors.system.ignoresSafeArea()) .frame(maxHeight: .infinity) .navigationTitle(VectorL10n.userSessionsOverviewTitle) .activityIndicator(show: viewModel.viewState.showLoadingIndicator) From 97c77c96f00466fe146b76dcb3380893e9f882c0 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 6 Sep 2022 16:55:02 +0200 Subject: [PATCH 13/35] Add changes --- changelog.d/6672.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6672.wip diff --git a/changelog.d/6672.wip b/changelog.d/6672.wip new file mode 100644 index 000000000..c04ba7598 --- /dev/null +++ b/changelog.d/6672.wip @@ -0,0 +1 @@ +Device manager: Add other sessions section read only in user sessions overview screen. From f50b58b252f98acf2b79dd648a54e112ccce07cb Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 10:50:50 +0200 Subject: [PATCH 14/35] Update RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- .../Service/MatrixSDK/UserSessionsOverviewService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift index e3670da92..59ccf1e02 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift @@ -60,7 +60,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol { private func setupInitialOverviewData() { let currentSessionInfo = self.getCurrentUserSessionInfoFromCache() - self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: [], inactiveSessionsInfo: [], otherSessionsInfo: []) + self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: [], inactiveSessionsInfo: [], otherSessionsInfo: []) } private func getCurrentUserSessionInfoFromCache() -> UserSessionInfo? { From a3a0e640b643abfa17cacc25d7827b7154b74853 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 10:51:24 +0200 Subject: [PATCH 15/35] Update RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- .../UserSessions/DeviceAvatar/DeviceAvatarView.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift index 9c54d2f40..cc78b727e 100644 --- a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift +++ b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift @@ -24,10 +24,8 @@ struct DeviceAvatarView: View { var viewData: DeviceAvatarViewData - var avatarSize: Float = 40.0 - var badgeSize: Float { - return 24 - } + var avatarSize: CGFloat = 40 + var badgeSize: CGFloat = 24 var body: some View { ZStack { From 8c63a588e4805c1ea25fff3c96a0caa3015f04c6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 10:52:29 +0200 Subject: [PATCH 16/35] Update RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- .../UserSessionsOverview/View/UserSessionListItem.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 701d0ca84..1fee5d4ce 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -72,7 +72,8 @@ struct UserSessionListItem: View { HStack { HStack(spacing: 18) { DeviceAvatarView(viewData: viewData.deviceAvatarViewData) - VStack(alignment: .leading, spacing: 2) { Text(sessionTitle) + VStack(alignment: .leading, spacing: 2) { + Text(sessionTitle) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) Text(sessionDetailsText) From 503669b9f31fe3a46d4e426f386a155f2c3e8277 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 10:52:37 +0200 Subject: [PATCH 17/35] Update RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- .../UserSessionsOverview/UserSessionsOverviewViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index 9e000bb94..0edaf96bb 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -72,7 +72,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess let unverifiedSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.unverifiedSessionsInfo) let inactiveSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.inactiveSessionsInfo) - var currentSessionViewData: UserSessionListItemViewData? + var currentSessionViewData: UserSessionListItemViewData? let otherSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.otherSessionsInfo) From e9504170fddb9a7b27dfacaba9f5d23bfecb281f Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 11:09:22 +0200 Subject: [PATCH 18/35] DeviceAvatarView: SImplify layout. --- .../DeviceAvatar/DeviceAvatarView.swift | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift index cc78b727e..a28618a41 100644 --- a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift +++ b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift @@ -28,33 +28,26 @@ struct DeviceAvatarView: View { var badgeSize: CGFloat = 24 var body: some View { - ZStack { - VStack { - VStack(alignment: .center) { - viewData.deviceType.image - } - .padding() + ZStack(alignment: .bottomTrailing) { + + // Device image + VStack(alignment: .center) { + viewData.deviceType.image } + .padding() .frame(maxWidth: CGFloat(avatarSize), maxHeight: CGFloat(avatarSize)) .background(theme.colors.system) .clipShape(Circle()) + // Verification badge if let isVerified = viewData.isVerified { - VStack(alignment: .trailing) { - Spacer() - HStack(alignment: .bottom) { - Spacer() - VStack() { - Image(isVerified ? Asset.Images.userSessionVerified.name : Asset.Images.userSessionUnverified.name) - } - .frame(maxWidth: CGFloat(badgeSize), maxHeight: CGFloat(badgeSize)) - .shapedBorder(color: theme.colors.system, borderWidth: 1, shape: Circle()) - .background(theme.colors.background) - .clipShape(Circle()) - } - .offset(x: 10, y: 8) - } + Image(isVerified ? Asset.Images.userSessionVerified.name : Asset.Images.userSessionUnverified.name) + .frame(maxWidth: CGFloat(badgeSize), maxHeight: CGFloat(badgeSize)) + .shapedBorder(color: theme.colors.system, borderWidth: 1, shape: Circle()) + .background(theme.colors.background) + .clipShape(Circle()) + .offset(x: 10, y: 8) } } .frame(maxWidth: CGFloat(avatarSize), maxHeight: CGFloat(avatarSize)) From bc1480f4a585739989ba2d1c5ba03cdecfc64991 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 11:21:05 +0200 Subject: [PATCH 19/35] UserSessionListItem: Improve layout. --- .../UserSessionsOverview/View/UserSessionListItem.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 1fee5d4ce..b916256c3 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -69,24 +69,25 @@ struct UserSessionListItem: View { // MARK: - Body var body: some View { - HStack { + Button(action: { onBackgroundTap?(self.viewData.sessionId) + }) { HStack(spacing: 18) { DeviceAvatarView(viewData: viewData.deviceAvatarViewData) VStack(alignment: .leading, spacing: 2) { Text(sessionTitle) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) + .multilineTextAlignment(.leading) + Text(sessionDetailsText) .font(theme.fonts.caption1) .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.leading) } } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 15) - .onTapGesture { - onBackgroundTap?(self.viewData.sessionId) - } } // MARK: - Private From 8d70b28ef696875433268fa9de43ce8f3ce91764 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 12:20:05 +0200 Subject: [PATCH 20/35] Create UserSessionNameFormatter to build user session name. --- .../UserSessionNameFormatter.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionNameFormatter.swift diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionNameFormatter.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionNameFormatter.swift new file mode 100644 index 000000000..0aed1082f --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionNameFormatter.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 Foundation + +/// Enables to build user session name +class UserSessionNameFormatter { + + /// Session name with client name and session display name + func sessionName(deviceType: DeviceType, sessionDisplayName: String?) -> String { + + let sessionName: String + + let clientName = deviceType.name + + if let sessionDisplayName = sessionDisplayName { + sessionName = VectorL10n.userSessionName(clientName, sessionDisplayName) + } else { + sessionName = clientName + } + + return sessionName + } +} From db32e1703d6a0789fdbb39cb2d33f4dc24e27bbc Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 12:20:56 +0200 Subject: [PATCH 21/35] Create UserSessionLastActivityFormatter to build last activity date string. --- .../UserSessionLastActivityFormatter.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionLastActivityFormatter.swift diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionLastActivityFormatter.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionLastActivityFormatter.swift new file mode 100644 index 000000000..891e0919b --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionLastActivityFormatter.swift @@ -0,0 +1,42 @@ +// +// 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 + +/// Enables to build last activity date string +class UserSessionLastActivityFormatter { + + // MARK: - Constants + + private static var lastActivityDateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + + // MARK: - Public + + /// Session last activity string + func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String { + + let date = Date(timeIntervalSince1970: lastActivityTimestamp) + + return UserSessionLastActivityFormatter.lastActivityDateFormatter.string(from: date) + } +} From cf4b3bd3c6cb4b3720c2deec708e5c10adbc387e Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 12:22:48 +0200 Subject: [PATCH 22/35] UserSessionListItemViewData: Handles session name and details string building. --- .../View/UserSessionListItem.swift | 56 +------------------ .../View/UserSessionListItemViewData.swift | 47 ++++++++++++---- 2 files changed, 37 insertions(+), 66 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index b916256c3..974f1b0bd 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -23,43 +23,7 @@ struct UserSessionListItem: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI - - private var sessionTitle: String { - - let sessionTitle: String - - let clientName = viewData.deviceType.name - - if let sessionName = viewData.sessionName { - sessionTitle = VectorL10n.userSessionName(clientName, sessionName) - } else { - sessionTitle = clientName - } - - return sessionTitle - } - - private var sessionDetailsText: String { - - let sessionDetailsString: String - - let sessionStatusText = viewData.isVerified ? VectorL10n.userSessionVerifiedShort : VectorL10n.userSessionUnverifiedShort - - var lastActivityDateString: String? - - if let lastActivityDate = viewData.lastActivityDate { - lastActivityDateString = self.lastActivityDateString(from: lastActivityDate) - } - if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false { - sessionDetailsString = VectorL10n.userSessionItemDetails(sessionStatusText, lastActivityDateString) - } else { - sessionDetailsString = sessionStatusText - } - - return sessionDetailsString - } - // MARK: Public let viewData: UserSessionListItemViewData @@ -74,12 +38,12 @@ struct UserSessionListItem: View { HStack(spacing: 18) { DeviceAvatarView(viewData: viewData.deviceAvatarViewData) VStack(alignment: .leading, spacing: 2) { - Text(sessionTitle) + Text(viewData.sessionName) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) .multilineTextAlignment(.leading) - Text(sessionDetailsText) + Text(viewData.sessionDetails) .font(theme.fonts.caption1) .foregroundColor(theme.colors.secondaryContent) .multilineTextAlignment(.leading) @@ -89,22 +53,6 @@ struct UserSessionListItem: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 15) } - - // MARK: - Private - - private func lastActivityDateString(from timestamp: TimeInterval) -> String? { - - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale.current - - let date = Date(timeIntervalSince1970: timestamp) - - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .short - dateFormatter.doesRelativeDateFormatting = true - - return dateFormatter.string(from: date) - } } struct UserSessionListPreview: View { diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift index c3c699789..4acac069e 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift @@ -19,6 +19,11 @@ import Foundation /// View data for UserSessionListItem struct UserSessionListItemViewData: Identifiable { + // MARK: - Constants + + private static let userSessionNameFormatter = UserSessionNameFormatter() + private static let lastActivityDateFormatter = UserSessionLastActivityFormatter() + // MARK: - Properties var id: String { @@ -27,35 +32,53 @@ struct UserSessionListItemViewData: Identifiable { let sessionId: String - let sessionName: String? + let sessionName: String - let deviceType: DeviceType - - let isVerified: Bool - - let lastActivityDate: TimeInterval? + let sessionDetails: String let deviceAvatarViewData: DeviceAvatarViewData // MARK: - Setup init(sessionId: String, - sessionName: String?, + sessionDisplayName: String?, deviceType: DeviceType, isVerified: Bool, lastActivityDate: TimeInterval?) { + self.sessionId = sessionId - self.sessionName = sessionName - self.deviceType = deviceType - self.isVerified = isVerified - self.lastActivityDate = lastActivityDate + self.sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName) + self.sessionDetails = Self.buildSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate) self.deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: isVerified) } + + // MARK: - Private + + private static func buildSessionDetails(isVerified: Bool, lastActivityDate: TimeInterval?) -> String { + + let sessionDetailsString: String + + let sessionStatusText = isVerified ? VectorL10n.userSessionVerifiedShort : VectorL10n.userSessionUnverifiedShort + + var lastActivityDateString: String? + + if let lastActivityDate = lastActivityDate { + lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityDate) + } + + if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false { + sessionDetailsString = VectorL10n.userSessionItemDetails(sessionStatusText, lastActivityDateString) + } else { + sessionDetailsString = sessionStatusText + } + + return sessionDetailsString + } } extension UserSessionListItemViewData { init(userSessionInfo: UserSessionInfo) { - self.init(sessionId: userSessionInfo.sessionId, sessionName: userSessionInfo.sessionName, deviceType: userSessionInfo.deviceType, isVerified: userSessionInfo.isVerified, lastActivityDate: userSessionInfo.lastSeenTimestamp) + self.init(sessionId: userSessionInfo.sessionId, sessionDisplayName: userSessionInfo.sessionName, deviceType: userSessionInfo.deviceType, isVerified: userSessionInfo.isVerified, lastActivityDate: userSessionInfo.lastSeenTimestamp) } } From e840c2e8c36c2a3681edcbcfa7e019585ab524d8 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 12:23:03 +0200 Subject: [PATCH 23/35] Update MockUserSessionsOverviewService. --- .../Service/Mock/MockUserSessionsOverviewService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift index c4cc729e1..f0aa43861 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift @@ -37,6 +37,6 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { UserSessionInfo(sessionId: "3", sessionName: "Android", deviceType: .mobile, isVerified: false, lastSeenIP: "3.0.0.3", lastSeenTimestamp: (Date().timeIntervalSince1970 - 10)) ] - self.lastOverviewData = UserSessionsOverviewData.init(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: unverifiedSessionsInfo, inactiveSessionsInfo: inactiveSessionsInfo, otherSessionsInfo: otherSessionsInfo) + self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: unverifiedSessionsInfo, inactiveSessionsInfo: inactiveSessionsInfo, otherSessionsInfo: otherSessionsInfo) } } From e37c3a9fa27b0471ce35ef1717d6f5fddb69ea68 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 14:10:41 +0200 Subject: [PATCH 24/35] Strings: Add comment for user session name format. --- Riot/Assets/en.lproj/Vector.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index f2dd394c9..011a610a5 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2365,7 +2365,9 @@ To enable access, tap Settings> Location and select Always"; "user_session_verified_short" = "Verified"; "user_session_unverified_short" = "Unverified"; +// First item is client name and second item is session display name "user_session_name" = "%@: %@"; + "user_session_item_details" = "%@ · Last activity %@"; "device_name_desktop" = "%@ Desktop"; From 790bc1e0844ac7a0e34dc5e11442ca15b8f54240 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 15:42:27 +0200 Subject: [PATCH 25/35] UserSessionListItem: Improve layout and handle separator. --- .../View/UserSessionListItem.swift | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 974f1b0bd..16e90d982 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -18,6 +18,15 @@ import SwiftUI struct UserSessionListItem: View { + // MARK: - Constants + + private enum LayoutConstants { + static let horizontalPadding: CGFloat = 15 + static let verticalPadding: CGFloat = 16 + static let avatarWidth: CGFloat = 40 + static let avatarRightMargin: CGFloat = 18 + } + // MARK: - Properties // MARK: Private @@ -35,23 +44,34 @@ struct UserSessionListItem: View { var body: some View { Button(action: { onBackgroundTap?(self.viewData.sessionId) }) { - HStack(spacing: 18) { - DeviceAvatarView(viewData: viewData.deviceAvatarViewData) - VStack(alignment: .leading, spacing: 2) { - Text(viewData.sessionName) - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) - .multilineTextAlignment(.leading) - - Text(viewData.sessionDetails) - .font(theme.fonts.caption1) - .foregroundColor(theme.colors.secondaryContent) - .multilineTextAlignment(.leading) + VStack(alignment: .leading, spacing: LayoutConstants.verticalPadding) { + HStack(spacing: LayoutConstants.avatarRightMargin) { + DeviceAvatarView(viewData: viewData.deviceAvatarViewData) + VStack(alignment: .leading, spacing: 2) { + Text(viewData.sessionName) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + .multilineTextAlignment(.leading) + + Text(viewData.sessionDetails) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.leading) + } } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, LayoutConstants.horizontalPadding) + + // Separator + // Note: Separator leading is matching the text leading, we could use alignment guide in the future + Rectangle() + .fill(theme.colors.quinaryContent) + .frame(maxWidth: .infinity, maxHeight: 1, alignment: .trailing) + .padding(.leading, LayoutConstants.horizontalPadding + LayoutConstants.avatarRightMargin + LayoutConstants.avatarWidth) } } .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 15) + .padding(.top, LayoutConstants.verticalPadding) } } @@ -60,7 +80,7 @@ struct UserSessionListPreview: View { let userSessionsOverviewService: UserSessionsOverviewServiceProtocol = MockUserSessionsOverviewService() var body: some View { - VStack(alignment: .leading, spacing: 14) { + VStack(alignment: .leading) { ForEach(userSessionsOverviewService.lastOverviewData.otherSessionsInfo) { userSessionInfo in let viewData = UserSessionListItemViewData(userSessionInfo: userSessionInfo) @@ -68,9 +88,7 @@ struct UserSessionListPreview: View { }) } - Spacer() } - .padding() } } From f2afcafdeb68b75cbe45694de3d302a16d6e82af Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 7 Sep 2022 15:47:03 +0200 Subject: [PATCH 26/35] UserSessionsOverview: Improve layout. --- .../View/UserSessionsOverview.swift | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 9a1719767..3e77aabf9 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -44,34 +44,7 @@ struct UserSessionsOverview: View { // Other sessions section if viewModel.viewState.otherSessionsViewData.isEmpty == false { - - VStack() { - - // Section header - VStack(alignment: .leading) { - Text(VectorL10n.userSessionsOverviewOtherSessionsSectionTitle) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.secondaryContent) - .padding(.bottom, 10) - - Text(VectorL10n.userSessionsOverviewOtherSessionsSectionInfo) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.secondaryContent) - .padding(.bottom, 11) - } - .padding(.horizontal, 16) - - // Device list - VStack(spacing: 16) { - ForEach(viewModel.viewState.otherSessionsViewData) { viewData in - UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in - viewModel.send(viewAction: .tapUserSession(sessionId)) - }) - } - } - .padding(.vertical, 16) - .background(theme.colors.background) - } + self.otherSessionsSection } } .background(theme.colors.system.ignoresSafeArea()) @@ -82,6 +55,34 @@ struct UserSessionsOverview: View { viewModel.send(viewAction: .viewAppeared) } } + + var otherSessionsSection: some View { + + SwiftUI.Section { + // Device list + LazyVStack() { + ForEach(viewModel.viewState.otherSessionsViewData) { viewData in + UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + viewModel.send(viewAction: .tapUserSession(sessionId)) + }) + } + } + .background(theme.colors.background) + } header: { + VStack(alignment: .leading) { + Text(VectorL10n.userSessionsOverviewOtherSessionsSectionTitle) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 10) + + Text(VectorL10n.userSessionsOverviewOtherSessionsSectionInfo) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 11) + } + .padding(.horizontal, 16) + } + } } // MARK: - Previews From eb64505a0ba1831f0c894c51fedbc837f95c80ae Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 8 Sep 2022 08:34:21 +0200 Subject: [PATCH 27/35] UserSessionsOverview: Improve other sessions list layout. --- .../UserSessionsOverview/View/UserSessionListItem.swift | 6 +++--- .../UserSessionsOverview/View/UserSessionsOverview.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 16e90d982..c2c4cff7a 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -66,12 +66,12 @@ struct UserSessionListItem: View { // Note: Separator leading is matching the text leading, we could use alignment guide in the future Rectangle() .fill(theme.colors.quinaryContent) - .frame(maxWidth: .infinity, maxHeight: 1, alignment: .trailing) + .frame(width: .infinity, height: 1.0, alignment: .trailing) .padding(.leading, LayoutConstants.horizontalPadding + LayoutConstants.avatarRightMargin + LayoutConstants.avatarWidth) } + .padding(.top, LayoutConstants.verticalPadding) } .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, LayoutConstants.verticalPadding) } } @@ -80,7 +80,7 @@ struct UserSessionListPreview: View { let userSessionsOverviewService: UserSessionsOverviewServiceProtocol = MockUserSessionsOverviewService() var body: some View { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { ForEach(userSessionsOverviewService.lastOverviewData.otherSessionsInfo) { userSessionInfo in let viewData = UserSessionListItemViewData(userSessionInfo: userSessionInfo) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 3e77aabf9..f4b49c4ea 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -60,7 +60,7 @@ struct UserSessionsOverview: View { SwiftUI.Section { // Device list - LazyVStack() { + LazyVStack(spacing: 0) { ForEach(viewModel.viewState.otherSessionsViewData) { viewData in UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in viewModel.send(viewAction: .tapUserSession(sessionId)) From 78c66b1dfc28e5406d56285a6f5e7a8a6a386da6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 8 Sep 2022 08:56:36 +0200 Subject: [PATCH 28/35] UserSessionListItem: Fix runtime issue. --- .../UserSessionsOverview/View/UserSessionListItem.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index c2c4cff7a..ae1b65893 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -66,7 +66,8 @@ struct UserSessionListItem: View { // Note: Separator leading is matching the text leading, we could use alignment guide in the future Rectangle() .fill(theme.colors.quinaryContent) - .frame(width: .infinity, height: 1.0, alignment: .trailing) + .frame(maxWidth: .infinity, alignment: .trailing) + .frame(height: 1.0) .padding(.leading, LayoutConstants.horizontalPadding + LayoutConstants.avatarRightMargin + LayoutConstants.avatarWidth) } .padding(.top, LayoutConstants.verticalPadding) From f0b74adc9ab77e2b8ae921087491eef7f9ac4450 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 8 Sep 2022 17:26:09 +0200 Subject: [PATCH 29/35] MockAppScreens: Add MockUserSessionsOverviewScreenState. --- RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift | 1 + .../Test/UI/UserSessionsOverviewUITests.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index a2f2b6602..e52ac8e29 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] = [ + MockUserSessionsOverviewScreenState.self, MockLiveLocationLabPromotionScreenState.self, MockLiveLocationSharingViewerScreenState.self, MockAuthenticationLoginScreenState.self, diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift index aa0cf5e87..136372d89 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift @@ -18,4 +18,5 @@ import XCTest import RiotSwiftUI class UserSessionsOverviewUITests: MockScreenTestCase { + // TODO: } From eb1c332109034f0def8b4ab28437661932faf95d Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Thu, 8 Sep 2022 18:49:41 +0200 Subject: [PATCH 30/35] UserSessionsOverviewViewModelResult: Remove useless case. --- .../Coordinator/UserSessionsOverviewCoordinator.swift | 6 ------ .../UserSessionsOverview/UserSessionsOverviewModels.swift | 1 - 2 files changed, 7 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index 1220de7c3..1494eb7c2 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -65,8 +65,6 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { switch result { case .cancel: self.completion?() - case .loadData: - self.loadData() case .showAllUnverifiedSessions: self.showAllUnverifiedSessions() case .showAllInactiveSessions: @@ -102,10 +100,6 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { loadingIndicator = nil } - private func loadData() { - // TODO - } - private func showAllUnverifiedSessions() { // TODO } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift index 438028b5f..5d01772fa 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -22,7 +22,6 @@ import Foundation enum UserSessionsOverviewViewModelResult { case cancel - case loadData case showAllUnverifiedSessions case showAllInactiveSessions case verifyCurrentSession From 17dc148cc73f6b2e600b02575ee583cacd718dcb Mon Sep 17 00:00:00 2001 From: gulekismail Date: Fri, 9 Sep 2022 11:08:04 +0300 Subject: [PATCH 31/35] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 2126ffd55..fbe364eb0 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.4 -CURRENT_PROJECT_VERSION = 1.9.4 +MARKETING_VERSION = 1.9.5 +CURRENT_PROJECT_VERSION = 1.9.5 From c913461f4d12dc9e32802d53b60f157ff9cc7a60 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Sat, 10 Sep 2022 20:42:12 +0300 Subject: [PATCH 32/35] Fix timeline items text height calculation --- Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m | 2 +- changelog.d/pr-6702.bugfix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/pr-6702.bugfix diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index 1a0998609..2cd44566e 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -534,7 +534,7 @@ CGFloat horizontalInset = measurementTextView.textContainer.lineFragmentPadding * 2; CGSize size = [attributedText boundingRectWithSize:CGSizeMake(_maxTextViewWidth - horizontalInset, CGFLOAT_MAX) - options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading + options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingUsesDeviceMetrics context:nil].size; //In iOS 7 and later, this method returns fractional sizes (in the size component of the returned rectangle); diff --git a/changelog.d/pr-6702.bugfix b/changelog.d/pr-6702.bugfix new file mode 100644 index 000000000..af2b8f154 --- /dev/null +++ b/changelog.d/pr-6702.bugfix @@ -0,0 +1 @@ +Fix timeline items text height calculation \ No newline at end of file From 394de8be1d9c603586f6b6988abd0efc3b04562d Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 12 Sep 2022 11:39:44 +0100 Subject: [PATCH 33/35] Update RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift --- .../UserSessions/DeviceAvatar/DeviceAvatarView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift index a28618a41..a24567171 100644 --- a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift +++ b/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift @@ -43,11 +43,11 @@ struct DeviceAvatarView: View { if let isVerified = viewData.isVerified { Image(isVerified ? Asset.Images.userSessionVerified.name : Asset.Images.userSessionUnverified.name) - .frame(maxWidth: CGFloat(badgeSize), maxHeight: CGFloat(badgeSize)) - .shapedBorder(color: theme.colors.system, borderWidth: 1, shape: Circle()) - .background(theme.colors.background) - .clipShape(Circle()) - .offset(x: 10, y: 8) + .frame(maxWidth: CGFloat(badgeSize), maxHeight: CGFloat(badgeSize)) + .shapedBorder(color: theme.colors.system, borderWidth: 1, shape: Circle()) + .background(theme.colors.background) + .clipShape(Circle()) + .offset(x: 10, y: 8) } } .frame(maxWidth: CGFloat(avatarSize), maxHeight: CGFloat(avatarSize)) From a3c410b28b31227858fbeadfe031d602b0fe042b Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 12 Sep 2022 13:32:01 +0100 Subject: [PATCH 34/35] version++ --- CHANGES.md | 11 +++++++++++ changelog.d/6672.wip | 1 - changelog.d/pr-6702.bugfix | 1 - 3 files changed, 11 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/6672.wip delete mode 100644 changelog.d/pr-6702.bugfix diff --git a/CHANGES.md b/CHANGES.md index 970c9527c..6d0f6fef5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +## Changes in 1.9.5 (2022-09-12) + +🐛 Bugfixes + +- Fix timeline items text height calculation ([#6702](https://github.com/vector-im/element-ios/pull/6702)) + +🚧 In development 🚧 + +- Device manager: Add other sessions section read only in user sessions overview screen. ([#6672](https://github.com/vector-im/element-ios/issues/6672)) + + ## Changes in 1.9.4 (2022-09-09) ✨ Features diff --git a/changelog.d/6672.wip b/changelog.d/6672.wip deleted file mode 100644 index c04ba7598..000000000 --- a/changelog.d/6672.wip +++ /dev/null @@ -1 +0,0 @@ -Device manager: Add other sessions section read only in user sessions overview screen. diff --git a/changelog.d/pr-6702.bugfix b/changelog.d/pr-6702.bugfix deleted file mode 100644 index af2b8f154..000000000 --- a/changelog.d/pr-6702.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix timeline items text height calculation \ No newline at end of file From 95cda637474f6c3e93d49347eacd979298dc3175 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Sep 2022 18:18:31 +0300 Subject: [PATCH 35/35] Replace attributed string height calculation with a more reliable implementation --- .../Models/Room/MXKRoomBubbleCellData.m | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index 2cd44566e..8ee0a6190 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -533,15 +533,7 @@ CGFloat verticalInset = measurementTextView.textContainerInset.top + measurementTextView.textContainerInset.bottom; CGFloat horizontalInset = measurementTextView.textContainer.lineFragmentPadding * 2; - CGSize size = [attributedText boundingRectWithSize:CGSizeMake(_maxTextViewWidth - horizontalInset, CGFLOAT_MAX) - options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingUsesDeviceMetrics - context:nil].size; - - //In iOS 7 and later, this method returns fractional sizes (in the size component of the returned rectangle); - // to use a returned size to size views, you must use raise its value to the nearest higher integer using the - // [ceil](https://developer.apple.com/documentation/kernel/1557272-ceil?changes=latest_major) function. - size.width = ceil(size.width); - size.height = ceil(size.height); + CGSize size = [self sizeForAttributedString:attributedText fittingWidth:_maxTextViewWidth - horizontalInset]; // The result is expected to contain the textView textContainer's paddings. Add them back if necessary if (removeVerticalInset == NO) { @@ -553,6 +545,27 @@ return size; } +// https://stackoverflow.com/questions/54497598/nsattributedstring-boundingrect-returns-wrong-height +- (CGSize)sizeForAttributedString:(NSAttributedString *)attributedString fittingWidth:(CGFloat)width +{ + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; + + CGRect boundingRect = CGRectMake(0.0, 0.0, width, CGFLOAT_MAX); + + NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:boundingRect.size]; + textContainer.lineFragmentPadding = 0; + + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + [layoutManager addTextContainer: textContainer]; + + [textStorage addLayoutManager:layoutManager]; + [layoutManager glyphRangeForBoundingRect:boundingRect inTextContainer:textContainer]; + + CGRect rect = [layoutManager usedRectForTextContainer:textContainer]; + + return CGRectIntegral(rect).size; +} + #pragma mark - Properties - (MXSession*)mxSession