diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/Contents.json new file mode 100644 index 000000000..e3af9f053 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_session_list_item_inactive_session.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/user_session_list_item_inactive_session.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/user_session_list_item_inactive_session.svg new file mode 100644 index 000000000..5aba5a38b --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/user_session_list_item_inactive_session.svg @@ -0,0 +1,3 @@ + + + diff --git a/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift b/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift new file mode 100644 index 000000000..f777b56d3 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift @@ -0,0 +1,33 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class InactiveUserSessionLastActivityFormatter { + private static var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.dateStyle = .medium + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + + func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String { + let date = Date(timeIntervalSince1970: lastActivityTimestamp) + + return InactiveUserSessionLastActivityFormatter.dateFormatter.string(from: date) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index f2729cf96..4d7293bc7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -40,7 +40,6 @@ struct UserOtherSessions: View { } header: { UserOtherSessionsHeaderView(viewData: header) .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16.0) .padding(.top, 24.0) } case .clearFilter: diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift index 11b3c4247..ac8f612d9 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift @@ -16,7 +16,7 @@ import SwiftUI -struct UserOtherSessionsHeaderViewData { +struct UserOtherSessionsHeaderViewData: Hashable { var title: String? var subtitle: String var iconName: String? @@ -24,6 +24,10 @@ struct UserOtherSessionsHeaderViewData { struct UserOtherSessionsHeaderView: View { + private var backgroundShape: RoundedRectangle { + RoundedRectangle(cornerRadius: 8) + } + @Environment(\.theme) private var theme let viewData: UserOtherSessionsHeaderViewData @@ -33,13 +37,15 @@ struct UserOtherSessionsHeaderView: View { if let iconName = viewData.iconName { Image(iconName) .foregroundColor(.red) + .frame(width: 40, height: 40) + .background(theme.colors.background) + .clipShape(backgroundShape) + .shapedBorder(color: theme.colors.quinaryContent, borderWidth: 1.0, shape: backgroundShape) } VStack(alignment: .leading, spacing: 0, content: { if let title = viewData.title { Text(title) - .font(.callout) - .textCase(.uppercase) - .font(theme.fonts.footnote) + .font(theme.fonts.calloutSB) .foregroundColor(theme.colors.primaryContent) .padding(.bottom, 9.0) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift index d94dbe6c9..319092ef8 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift @@ -48,7 +48,7 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { deviceType: .desktop, isVerified: true, lastSeenIP: "1.0.0.1", - lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000, + lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000, isActive: false, isCurrent: false), UserSessionInfo(id: "2", diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index fb0ef5e77..53ec3289b 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -100,6 +100,6 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess extension Collection where Element == UserSessionInfo { func asViewData() -> [UserSessionListItemViewData] { - map { UserSessionListItemViewData(session: $0) } + map { UserSessionListItemViewDataFactory().create(from: $0)} } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 51a698541..001b658b5 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -42,11 +42,16 @@ struct UserSessionListItem: View { .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) .multilineTextAlignment(.leading) - - Text(viewData.sessionDetails) - .font(theme.fonts.caption1) - .foregroundColor(theme.colors.secondaryContent) - .multilineTextAlignment(.leading) + HStack { + if let sessionDetailsIcon = viewData.sessionDetailsIcon { + Image(sessionDetailsIcon) + .padding(.leading, 2) + } + Text(viewData.sessionDetails) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.leading) + } } } .frame(maxWidth: .infinity, alignment: .leading) @@ -69,7 +74,7 @@ struct UserSessionListPreview: View { var body: some View { VStack(alignment: .leading, spacing: 0) { ForEach(userSessionsOverviewService.overviewData.otherSessions) { userSessionInfo in - let viewData = UserSessionListItemViewData(session: userSessionInfo) + let viewData = UserSessionListItemViewDataFactory().create(from: userSessionInfo) UserSessionListItem(viewData: viewData, onBackgroundTap: { _ in diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift index ad470b305..f050def9b 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift @@ -18,61 +18,17 @@ import Foundation /// View data for UserSessionListItem struct UserSessionListItemViewData: Identifiable, Hashable { - private static let userSessionNameFormatter = UserSessionNameFormatter() - private static let lastActivityDateFormatter = UserSessionLastActivityFormatter() - var id: String { sessionId } let sessionId: String - + let sessionName: String let sessionDetails: String let deviceAvatarViewData: DeviceAvatarViewData - init(sessionId: String, - sessionDisplayName: String?, - deviceType: DeviceType, - isVerified: Bool, - lastActivityDate: TimeInterval?) { - self.sessionId = sessionId - sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName) - sessionDetails = Self.buildSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate) - 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(session: UserSessionInfo) { - self.init(sessionId: session.id, - sessionDisplayName: session.name, - deviceType: session.deviceType, - isVerified: session.isVerified, - lastActivityDate: session.lastSeenTimestamp) - } + let sessionDetailsIcon: String? } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift new file mode 100644 index 000000000..2e6bb38a4 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift @@ -0,0 +1,80 @@ +// +// 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 + +struct UserSessionListItemViewDataFactory { + + private static let userSessionNameFormatter = UserSessionNameFormatter() + private static let lastActivityDateFormatter = UserSessionLastActivityFormatter() + private static let inactiveSessionDateFormatter = InactiveUserSessionLastActivityFormatter() + + func create(from session: UserSessionInfo) -> UserSessionListItemViewData { + let sessionName = UserSessionListItemViewDataFactory.userSessionNameFormatter.sessionName(deviceType: session.deviceType, + sessionDisplayName: session.name) + let sessionDetails = buildSessionDetails(isVerified: session.isVerified, + lastActivityDate: session.lastSeenTimestamp, + isActive: session.isActive) + let deviceAvatarViewData = DeviceAvatarViewData(deviceType: session.deviceType, + isVerified: session.isVerified) + return UserSessionListItemViewData(sessionId: session.id, + sessionName: sessionName, + sessionDetails: sessionDetails, + deviceAvatarViewData: deviceAvatarViewData, + sessionDetailsIcon: getSessionDetailsIcon(isActive: session.isActive)) + } + + private func buildSessionDetails(isVerified: Bool, lastActivityDate: TimeInterval?, isActive: Bool) -> String { + if isActive { + return activeSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate) + } else { + return inactiveSessionDetails(lastActivityDate: lastActivityDate) + } + } + + private func inactiveSessionDetails(lastActivityDate: TimeInterval?) -> String { + if let lastActivityDate = lastActivityDate { + let lastActivityDateString = Self.inactiveSessionDateFormatter.lastActivityDateString(from: lastActivityDate) + return "Inactive for 90+ days (\(lastActivityDateString))" + } + return "Inactive for 90+ days" + } + + private func activeSessionDetails(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 + } + + private func getSessionDetailsIcon(isActive: Bool) -> String? { + isActive ? nil : Asset.Images.userSessionListItemInactiveSession.name + } + +}