mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Add encryption history banner and update session verification banner strings. (#1250)
* Add a hard wall on back-pagination in encrypted rooms. Until the app supports key backup, the history will never be decryptable. * Fix initial display of the timeline table view. * Update session verification banner strings.
This commit is contained in:
@@ -22,6 +22,7 @@ final class AppSettings {
|
||||
private enum UserDefaultsKeys: String {
|
||||
case lastVersionLaunched
|
||||
case seenInvites
|
||||
case lastLoginDate
|
||||
case migratedAccounts
|
||||
case timelineStyle
|
||||
case analyticsConsentState
|
||||
@@ -105,6 +106,13 @@ final class AppSettings {
|
||||
/// Any pre-defined static client registrations for OIDC issuers.
|
||||
let oidcStaticRegistrations: [URL: String] = ["https://id.thirdroom.io/realms/thirdroom": "elementx"]
|
||||
|
||||
/// The date that the call to `/login` completed successfully. This is used to put
|
||||
/// a hard wall on the history of encrypted messages until we have key backup.
|
||||
///
|
||||
/// Not a multi-account aware setting as key backup will come before multi-account.
|
||||
@UserPreference(key: UserDefaultsKeys.lastLoginDate, defaultValue: nil, storageType: .userDefaults(store))
|
||||
var lastLoginDate: Date?
|
||||
|
||||
/// A dictionary of accounts that have performed an initial sync through their proxy.
|
||||
///
|
||||
/// This is a temporary workaround. In the future we should be able to receive a signal from the
|
||||
|
||||
@@ -271,15 +271,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
let userId = userSession.clientProxy.userID
|
||||
let userID = userSession.clientProxy.userID
|
||||
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userId))
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
||||
|
||||
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(userId: userId,
|
||||
roomProxy: roomProxy,
|
||||
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
self.timelineController = timelineController
|
||||
|
||||
@@ -718,6 +718,8 @@ public enum L10n {
|
||||
public static var screenRoomDetailsTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_topic_title") }
|
||||
/// Updating room…
|
||||
public static var screenRoomDetailsUpdatingRoom: String { return L10n.tr("Localizable", "screen_room_details_updating_room") }
|
||||
/// Message history is currently unavailable in this room
|
||||
public static var screenRoomEncryptedHistoryBanner: String { return L10n.tr("Localizable", "screen_room_encrypted_history_banner") }
|
||||
/// Failed processing media to upload, please try again.
|
||||
public static var screenRoomErrorFailedProcessingMedia: String { return L10n.tr("Localizable", "screen_room_error_failed_processing_media") }
|
||||
/// Could not retrieve user details
|
||||
@@ -848,9 +850,9 @@ public enum L10n {
|
||||
public static var screenWaitlistTitle: String { return L10n.tr("Localizable", "screen_waitlist_title") }
|
||||
/// You're in.
|
||||
public static var screenWaitlistTitleSuccess: String { return L10n.tr("Localizable", "screen_waitlist_title_success") }
|
||||
/// Looks like you’re using a new device. Verify it’s you to access your encrypted messages.
|
||||
/// Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards.
|
||||
public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") }
|
||||
/// Access your message history
|
||||
/// Verify it’s you
|
||||
public static var sessionVerificationBannerTitle: String { return L10n.tr("Localizable", "session_verification_banner_title") }
|
||||
/// Rageshake
|
||||
public static var settingsRageshake: String { return L10n.tr("Localizable", "settings_rageshake") }
|
||||
|
||||
@@ -205,6 +205,8 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func userHasSignedIn(userSession: UserSessionProtocol) {
|
||||
appSettings.lastLoginDate = .now
|
||||
|
||||
showAnalyticsPromptIfNeeded { [weak self] in
|
||||
guard let self else { return }
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright 2023 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
|
||||
|
||||
/// The item shown when all previous items are un-decryptable due to
|
||||
/// key backup not yet being supported in the app.
|
||||
struct EncryptedHistoryRoomTimelineView: View {
|
||||
let timelineItem: EncryptedHistoryRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
Label {
|
||||
Text(L10n.screenRoomEncryptedHistoryBanner)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.compound.textInfoPrimary)
|
||||
} icon: {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.foregroundColor(.compound.iconInfoPrimary)
|
||||
}
|
||||
.labelStyle(EncryptedHistoryLabelStyle())
|
||||
.padding(16)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.compound.bgInfoSubtle)
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.compound.borderInfoSubtle)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
|
||||
private struct EncryptedHistoryLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
configuration.icon
|
||||
configuration.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EncryptedHistoryRoomTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let item = EncryptedHistoryRoomTimelineItem(id: UUID().uuidString)
|
||||
EncryptedHistoryRoomTimelineView(timelineItem: item)
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,13 @@ class TimelineTableViewController: UIViewController {
|
||||
// for each possible cell, causing layout issues
|
||||
tableView.accessibilityElementsHidden = Tests.shouldDisableTimelineAccessibility
|
||||
|
||||
tableView.publisher(for: \.contentSize)
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] _ in
|
||||
self?.updateTopPadding()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
scrollToBottomPublisher
|
||||
.sink { [weak self] _ in
|
||||
self?.scrollToBottom(animated: true)
|
||||
@@ -242,9 +249,11 @@ class TimelineTableViewController: UIViewController {
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(timelineItemsIDs)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
|
||||
// Probably redundant now we observe content size changes…
|
||||
// Leaving in place for the release and will reassess after.
|
||||
updateTopPadding()
|
||||
|
||||
|
||||
if previousLayout.isBottomVisible {
|
||||
scrollToBottom(animated: false)
|
||||
} else if let pinnedItem = previousLayout.pinnedItem {
|
||||
@@ -283,7 +292,8 @@ class TimelineTableViewController: UIViewController {
|
||||
let contentHeight = tableView.contentSize.height - headerHeight
|
||||
let newHeight = max(0, tableView.visibleSize.height - contentHeight)
|
||||
|
||||
guard newHeight != headerHeight else { return }
|
||||
// Round the check to account floating point accuracy during keyboard appearance.
|
||||
guard newHeight.rounded() != headerHeight.rounded() else { return }
|
||||
|
||||
if newHeight > 0 {
|
||||
let frame = CGRect(origin: .zero, size: CGSize(width: tableView.contentSize.width, height: newHeight))
|
||||
|
||||
+1
-2
@@ -17,8 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
struct MockRoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
|
||||
func buildRoomTimelineController(userId: String,
|
||||
roomProxy: RoomProxyProtocol,
|
||||
func buildRoomTimelineController(roomProxy: RoomProxyProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol {
|
||||
let timelineController = MockRoomTimelineController()
|
||||
|
||||
@@ -19,7 +19,6 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let userId: String
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let timelineProvider: RoomTimelineProviderProtocol
|
||||
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
|
||||
@@ -42,12 +41,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
roomProxy.id
|
||||
}
|
||||
|
||||
init(userId: String,
|
||||
roomProxy: RoomProxyProtocol,
|
||||
init(roomProxy: RoomProxyProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
appSettings: AppSettings) {
|
||||
self.userId = userId
|
||||
self.roomProxy = roomProxy
|
||||
timelineProvider = roomProxy.timelineProvider
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
@@ -246,6 +243,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
var canBackPaginate = true
|
||||
var isBackPaginating = false
|
||||
var lastEncryptedHistoryItemIndex: Int?
|
||||
|
||||
let collapsibleChunks = timelineProvider.itemsPublisher.value.groupBy { isItemCollapsible($0) }
|
||||
|
||||
@@ -264,6 +262,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
isBackPaginating = true
|
||||
} else if timelineItem is TimelineStartRoomTimelineItem {
|
||||
canBackPaginate = false
|
||||
} else if timelineItem is EncryptedHistoryRoomTimelineItem {
|
||||
canBackPaginate = false
|
||||
}
|
||||
|
||||
return timelineItem
|
||||
@@ -280,11 +280,21 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
continue
|
||||
}
|
||||
|
||||
if timelineItem is EncryptedHistoryRoomTimelineItem {
|
||||
lastEncryptedHistoryItemIndex = newTimelineItems.endIndex
|
||||
}
|
||||
|
||||
newTimelineItems.append(timelineItem)
|
||||
} else {
|
||||
newTimelineItems.append(CollapsibleTimelineItem(items: items))
|
||||
}
|
||||
}
|
||||
|
||||
if let lastEncryptedHistoryItemIndex {
|
||||
// Remove everything up to the last encrypted history item.
|
||||
// It only contains encrypted messages, state changes and date separators.
|
||||
newTimelineItems.removeFirst(lastEncryptedHistoryItemIndex)
|
||||
}
|
||||
|
||||
timelineItems = newTimelineItems
|
||||
|
||||
@@ -298,6 +308,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
case .event(let eventTimelineItem):
|
||||
let timelineItem = timelineItemFactory.buildTimelineItem(for: eventTimelineItem)
|
||||
|
||||
if timelineItem is EncryptedRoomTimelineItem, isItemInEncryptionHistory(eventTimelineItem) {
|
||||
return EncryptedHistoryRoomTimelineItem(id: eventTimelineItem.id)
|
||||
}
|
||||
|
||||
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol {
|
||||
// Avoid fetching this over and over again as it changes states if it keeps failing to load
|
||||
// Errors will be handled again on appearance
|
||||
@@ -326,6 +340,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not a specific item is part of the room's history that can't be decrypted due
|
||||
/// to the lack of key-backup. This is handled differently so we only show a single item.
|
||||
private func isItemInEncryptionHistory(_ itemProxy: EventTimelineItemProxy) -> Bool {
|
||||
guard roomProxy.isEncrypted, let lastLoginDate = appSettings.lastLoginDate else { return false }
|
||||
return itemProxy.timestamp < lastLoginDate
|
||||
}
|
||||
|
||||
private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool {
|
||||
if !appSettings.shouldCollapseRoomStateEvents {
|
||||
return false
|
||||
|
||||
+2
-4
@@ -17,12 +17,10 @@
|
||||
import Foundation
|
||||
|
||||
struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
|
||||
func buildRoomTimelineController(userId: String,
|
||||
roomProxy: RoomProxyProtocol,
|
||||
func buildRoomTimelineController(roomProxy: RoomProxyProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol {
|
||||
RoomTimelineController(userId: userId,
|
||||
roomProxy: roomProxy,
|
||||
RoomTimelineController(roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: mediaProvider,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
+1
-2
@@ -18,8 +18,7 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineControllerFactoryProtocol {
|
||||
func buildRoomTimelineController(userId: String,
|
||||
roomProxy: RoomProxyProtocol,
|
||||
func buildRoomTimelineController(roomProxy: RoomProxyProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright 2023 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 EncryptedHistoryRoomTimelineItem: DecorationTimelineItemProtocol, Identifiable, Hashable {
|
||||
let id: String
|
||||
}
|
||||
@@ -55,6 +55,8 @@ struct RoomTimelineItemView: View {
|
||||
UnsupportedRoomTimelineView(timelineItem: item)
|
||||
case .timelineStart(let item):
|
||||
TimelineStartRoomTimelineView(timelineItem: item)
|
||||
case .encryptedHistory(let item):
|
||||
EncryptedHistoryRoomTimelineView(timelineItem: item)
|
||||
case .state(let item):
|
||||
StateRoomTimelineView(timelineItem: item)
|
||||
case .group(let item):
|
||||
|
||||
@@ -58,6 +58,7 @@ enum RoomTimelineItemType: Equatable {
|
||||
case sticker(StickerRoomTimelineItem)
|
||||
case unsupported(UnsupportedRoomTimelineItem)
|
||||
case timelineStart(TimelineStartRoomTimelineItem)
|
||||
case encryptedHistory(EncryptedHistoryRoomTimelineItem)
|
||||
case state(StateRoomTimelineItem)
|
||||
case group(CollapsibleTimelineItem)
|
||||
case location(LocationRoomTimelineItem)
|
||||
@@ -95,6 +96,8 @@ enum RoomTimelineItemType: Equatable {
|
||||
self = .unsupported(item)
|
||||
case let item as TimelineStartRoomTimelineItem:
|
||||
self = .timelineStart(item)
|
||||
case let item as EncryptedHistoryRoomTimelineItem:
|
||||
self = .encryptedHistory(item)
|
||||
case let item as StateRoomTimelineItem:
|
||||
self = .state(item)
|
||||
case let item as CollapsibleTimelineItem:
|
||||
@@ -123,6 +126,7 @@ enum RoomTimelineItemType: Equatable {
|
||||
.sticker(let item as RoomTimelineItemProtocol),
|
||||
.unsupported(let item as RoomTimelineItemProtocol),
|
||||
.timelineStart(let item as RoomTimelineItemProtocol),
|
||||
.encryptedHistory(let item as RoomTimelineItemProtocol),
|
||||
.state(let item as RoomTimelineItemProtocol),
|
||||
.group(let item as RoomTimelineItemProtocol),
|
||||
.location(let item as RoomTimelineItemProtocol):
|
||||
@@ -137,7 +141,7 @@ enum RoomTimelineItemType: Equatable {
|
||||
return true
|
||||
case .redacted, .encrypted, .unsupported, .state: // Event based items that aren't reactable
|
||||
return false
|
||||
case .timelineStart, .separator, .readMarker, .paginationIndicator: // Virtual items are never reactable
|
||||
case .timelineStart, .encryptedHistory, .separator, .readMarker, .paginationIndicator: // Virtual items are never reactable
|
||||
return false
|
||||
case .group:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user