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:
Doug
2023-07-05 14:08:31 +01:00
committed by GitHub
parent 29d90dd29e
commit 6b8b5a75b3
16 changed files with 172 additions and 36 deletions
@@ -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
+4 -2
View File
@@ -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)
@@ -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))
@@ -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
@@ -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)
@@ -18,8 +18,7 @@ import Foundation
@MainActor
protocol RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(userId: String,
roomProxy: RoomProxyProtocol,
func buildRoomTimelineController(roomProxy: RoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol
}
@@ -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