* provider can now check the current session

* More testable code

* created the test condition

* it works but not always not sure why, need to dig deeper

* sadly we need to use textkit 1 to solve this issue

* removed developer option screen test

* this experimental solution kinda works but I need a way to pill recomputation is weird

* format

* display improvement

* better and faster solution

* pilished the code

* better coloring

* swift format

* just need to solve the caching issue

* fix caching issue

* tests done!

* changelog

* pr comments addressed

* all pr comments addressed

* docs

* line lenght

* updated tests and fixed a parsing permalink issue

* MentionBuilder

* pr comments

* swiftformat

* code blocks should not have links

* Update ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* Update ElementX/Sources/Services/Client/ClientProxy.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* Update UnitTests/Sources/AttributedStringBuilderTests.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* Update ElementX/Sources/UITests/UITestsAppCoordinator.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* Update ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* Update ElementX/Sources/Other/Pills/MentionBuilder.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* pr comments

* swiftformat

---------

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Mauro
2023-09-27 19:27:07 +02:00
committed by GitHub
parent 8d4c5e2885
commit 7bde419e85
49 changed files with 530 additions and 97 deletions
@@ -34,7 +34,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
private var userSession: UserSessionProtocol? {
didSet {
userSessionObserver?.cancel()
if userSession != nil {
configureNotificationManager()
observeUserSessionChanges()
@@ -41,6 +41,7 @@ final class AppSettings {
case hasShownWelcomeScreen
case swiftUITimelineEnabled
case voiceMessageEnabled
case mentionsEnabled
}
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
@@ -248,4 +249,7 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.voiceMessageEnabled, defaultValue: false, storageType: .userDefaults(store))
var voiceMessageEnabled
@UserPreference(key: UserDefaultsKeys.mentionsEnabled, defaultValue: false, storageType: .userDefaults(store))
var mentionsEnabled
}
@@ -23,9 +23,9 @@ struct Application: App {
private let appCoordinator: AppCoordinatorProtocol
init() {
if Tests.isRunningUITests {
if ProcessInfo.isRunningUITests {
appCoordinator = UITestsAppCoordinator()
} else if Tests.isRunningUnitTests {
} else if ProcessInfo.isRunningUnitTests {
appCoordinator = UnitTestsAppCoordinator()
} else {
appCoordinator = AppCoordinator()
@@ -59,6 +59,6 @@ struct Application: App {
}
private var shouldHideStatusBar: Bool {
Tests.isRunningUITests
ProcessInfo.isRunningUITests
}
}
@@ -311,7 +311,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
mediaProvider: userSession.mediaProvider,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL,
mentionBuilder: MentionBuilder(mentionsEnabled: appSettings.mentionsEnabled)),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID),
appSettings: appSettings)
@@ -261,7 +261,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private func presentHomeScreen() {
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
bugReportService: bugReportService,
navigationStackCoordinator: detailNavigationStackCoordinator,
selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher())
@@ -1,4 +1,4 @@
// Generated using Sourcery 2.0.3 — https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.1.1 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable all
@@ -23,6 +23,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
private let temporaryCodeBlockMarkingColor = UIColor.cyan
private let linkColor = UIColor.blue
private let permalinkBaseURL: URL
private let mentionBuilder: MentionBuilderProtocol
private static var cache = LRUCache<String, AttributedString>(countLimit: 1000)
@@ -30,8 +31,9 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
cache.removeAllValues()
}
init(permalinkBaseURL: URL) {
init(permalinkBaseURL: URL, mentionBuilder: MentionBuilderProtocol) {
self.permalinkBaseURL = permalinkBaseURL
self.mentionBuilder = mentionBuilder
}
func fromPlain(_ string: String?) -> AttributedString? {
@@ -45,6 +47,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
let mutableAttributedString = NSMutableAttributedString(string: string)
addLinks(mutableAttributedString)
detectPermalinks(mutableAttributedString)
removeLinkColors(mutableAttributedString)
let result = try? AttributedString(mutableAttributedString, including: \.elementX)
@@ -97,10 +100,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
removeDefaultForegroundColor(mutableAttributedString)
addLinks(mutableAttributedString)
detectPermalinks(mutableAttributedString)
removeLinkColors(mutableAttributedString)
replaceMarkedBlockquotes(mutableAttributedString)
replaceMarkedCodeBlocks(mutableAttributedString)
detectPermalinks(mutableAttributedString)
removeLinkColors(mutableAttributedString)
removeDTCoreTextArtifacts(mutableAttributedString)
let result = try? AttributedString(mutableAttributedString, including: \.elementX)
@@ -140,6 +143,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
if let value = value as? UIColor,
value == temporaryCodeBlockMarkingColor {
attributedString.addAttribute(.backgroundColor, value: UIColor(.compound._bgCodeBlock) as Any, range: range)
// Codeblocks should not have links
attributedString.removeAttribute(.link, range: range)
}
}
}
@@ -177,7 +182,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
guard let matchRange = Range(match.range, in: string) else {
return
}
var hasLink = false
attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in
if value != nil {
@@ -196,7 +201,9 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
link.insert(contentsOf: "https://", at: link.startIndex)
}
attributedString.addAttribute(.link, value: link as Any, range: match.range)
if let url = URL(string: link) {
attributedString.addAttribute(.link, value: url, range: match.range)
}
}
}
@@ -206,7 +213,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
if let url = value as? URL {
switch PermalinkBuilder.detectPermalink(in: url, baseURL: permalinkBaseURL) {
case .userIdentifier(let identifier):
attributedString.addAttributes([.MatrixUserID: identifier], range: range)
mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: identifier)
case .roomIdentifier(let identifier):
attributedString.addAttributes([.MatrixRoomID: identifier], range: range)
case .roomAlias(let alias):
@@ -280,3 +287,7 @@ extension NSAttributedString.Key {
static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name)
static let MatrixEventID: NSAttributedString.Key = .init(rawValue: EventIDAttribute.name)
}
protocol MentionBuilderProtocol {
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String)
}
@@ -0,0 +1,49 @@
//
// 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
import UIKit
struct MentionBuilder: MentionBuilderProtocol {
// Can be removed when mentions are enabled by default
let mentionsEnabled: Bool
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) {
guard mentionsEnabled else {
attributedString.addAttributes([.MatrixUserID: userID], range: range)
return
}
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
let blockquote = attributes[.MatrixBlockquote]
let attachmentData = PillTextAttachmentData(type: .user(userID: userID), font: font)
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
attributedString.addAttributes([.MatrixUserID: userID], range: range)
return
}
var attributesToAdd: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID]
if let blockquote {
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
attributesToAdd[.MatrixBlockquote] = blockquote
}
let attachmentString = NSMutableAttributedString(attachment: attachment)
attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length))
attributedString.replaceCharacters(in: range, with: attachmentString)
}
}
+56 -9
View File
@@ -15,21 +15,49 @@
//
import SwiftUI
import UIKit
final class MessageTextView: UITextView {
var roomContext: RoomScreenViewModel.Context?
var updateClosure: (() -> Void)?
// This prevents the magnifying glass from showing up
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
gestureRecognizer as? UILongPressGestureRecognizer == nil
}
func invalidateTextAttachmentsDisplay(update: Bool) {
attributedText.enumerateAttribute(.attachment,
in: NSRange(location: 0, length: attributedText.length),
options: []) { value, range, _ in
guard value != nil else {
return
}
self.layoutManager.invalidateDisplay(forCharacterRange: range)
if update {
updateClosure?()
}
}
}
// Required to setup the first rendering of the pill view
override func layoutSubviews() {
invalidateTextAttachmentsDisplay(update: false)
super.layoutSubviews()
}
}
struct MessageText: UIViewRepresentable {
@Environment(\.openURL) private var openURLAction: OpenURLAction
let attributedString: AttributedString
@EnvironmentObject private var viewModel: RoomScreenViewModel.Context
@State var attributedString: AttributedString
func makeUIView(context: Context) -> MessageTextView {
let textView = MessageTextView()
// Need to use TextKit 1 for mentions
let textView = MessageTextView(usingTextLayoutManager: false)
textView.roomContext = viewModel
textView.updateClosure = {
attributedString = AttributedString(textView.attributedText)
}
textView.isEditable = false
textView.isScrollEnabled = false
textView.adjustsFontForContentSizeCategory = true
@@ -46,7 +74,7 @@ struct MessageText: UIViewRepresentable {
textView.contentInsetAdjustmentBehavior = .never
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.textLayoutManager?.usesFontLeading = false
textView.layoutManager.usesFontLeading = false
textView.backgroundColor = .clear
textView.attributedText = NSAttributedString(attributedString)
textView.delegate = context.coordinator
@@ -94,9 +122,20 @@ struct MessageText_Previews: PreviewProvider, TestablePreview {
container.font = UIFont.preferredFont(forTextStyle: .body)
return container
}()
private static let attributedString = AttributedString("Hello World! Hello world! Hello world! Hello world! Hello World! Hellooooooooooooooooooooooo Woooooooooooooooooooooorld", attributes: defaultFontContainer)
private static let attributedStringWithAttachment = "Hello " + AttributedString(NSAttributedString(attachment: PillTextAttachment(data: Data(), ofType: InfoPlistReader.main.pillsUTType))) + " World!"
private static let attributedStringWithAttachment: AttributedString = {
let testData = PillTextAttachmentData(type: .user(userID: "@alice:example.com"), font: .preferredFont(forTextStyle: .body))
guard let attachment = PillTextAttachment(attachmentData: testData) else {
return AttributedString()
}
var attributedString = "Hello test test test " + AttributedString(NSAttributedString(attachment: attachment)) + " World!"
attributedString
.mergeAttributes(defaultFontContainer)
return attributedString
}()
private static let htmlStringWithQuote =
"""
@@ -106,28 +145,36 @@ struct MessageText_Previews: PreviewProvider, TestablePreview {
private static let htmlStringWithList = "<p>This is a list</p>\n<ul>\n<li>One</li>\n<li>Two</li>\n<li>And number 3</li>\n</ul>\n"
private static let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL)
private static let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true))
static var attachmentPreview: some View {
MessageText(attributedString: attributedStringWithAttachment)
.border(Color.purple)
.environmentObject(RoomScreenViewModel.mock.context)
}
static var previews: some View {
MessageText(attributedString: attributedString)
.border(Color.purple)
.previewDisplayName("Custom Text")
.environmentObject(RoomScreenViewModel.mock.context)
// For comparison
Text(attributedString)
.border(Color.purple)
.previewDisplayName("SwiftUI Default Text")
MessageText(attributedString: attributedStringWithAttachment)
.border(Color.purple)
attachmentPreview
.previewDisplayName("Custom Attachment")
if let attributedString = attributedStringBuilder.fromHTML(htmlStringWithQuote) {
MessageText(attributedString: attributedString)
.border(Color.purple)
.previewDisplayName("With block quote")
.environmentObject(RoomScreenViewModel.mock.context)
}
if let attributedString = attributedStringBuilder.fromHTML(htmlStringWithList) {
MessageText(attributedString: attributedString)
.border(Color.purple)
.previewDisplayName("With list")
.environmentObject(RoomScreenViewModel.mock.context)
}
}
}
@@ -15,21 +15,52 @@
//
import SwiftUI
import SwiftUIIntrospect
import UIKit
final class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
private weak var messageTextView: MessageTextView?
// MARK: - Override
override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) {
super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)
// Keep a reference to the parent text view for size adjustments and pills flushing.
messageTextView = parentView?.superview as? MessageTextView
tracksTextAttachmentViewBounds = true
}
@MainActor
override func loadView() {
super.loadView()
guard textAttachment is PillTextAttachment else {
MXLog.failure("[PillAttachmentViewProvider]: attachment is missing or not of expected class")
guard let textAttachmentData = (textAttachment as? PillTextAttachment)?.pillData else {
MXLog.failure("[PillAttachmentViewProvider]: attachment is missing data or not of expected class")
return
}
let view = PillView()
let viewModel: PillContext
let imageProvider: ImageProviderProtocol?
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
// The mock viewModel simulates the loading logic for testing purposes
viewModel = PillContext.mock(type: .loadUser)
imageProvider = MockMediaProvider()
} else if let roomContext = messageTextView?.roomContext {
viewModel = PillContext(roomContext: roomContext, data: textAttachmentData)
imageProvider = roomContext.imageProvider
} else {
MXLog.failure("[PillAttachmentViewProvider]: missing room context")
return
}
let view = PillView(imageProvider: imageProvider, viewModel: viewModel) { [weak self] in
self?.messageTextView?.invalidateTextAttachmentsDisplay(update: true)
}
let controller = UIHostingController(rootView: view)
controller.view.backgroundColor = .clear
// This allows the text view to handle it as a link
controller.view.isUserInteractionEnabled = false
self.view = controller.view
}
}
@@ -0,0 +1,110 @@
//
// 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 Combine
import Foundation
@MainActor
final class PillContext: ObservableObject {
enum PillViewState {
case loading(contentID: String)
case loaded(contentID: String, name: String, avatarURL: URL?)
}
@Published private var state: PillViewState
var url: URL? {
switch state {
case .loading:
return nil
case .loaded(_, _, let url):
return url
}
}
var name: String? {
switch state {
case .loading:
return nil
case .loaded(_, let name, _):
return name
}
}
var displayText: String {
switch state {
case .loaded(_, let name, _):
return name
case .loading(let contentID):
return contentID
}
}
var contentID: String {
switch state {
case .loaded(let contentID, _, _), .loading(let contentID):
return contentID
}
}
private var cancellable: AnyCancellable?
init(roomContext: RoomScreenViewModel.Context, data: PillTextAttachmentData) {
switch data.type {
case let .user(id):
if let profile = roomContext.viewState.members[id] {
state = .loaded(contentID: id, name: profile.displayName ?? id, avatarURL: profile.avatarURL)
} else {
state = .loading(contentID: id)
cancellable = roomContext.$viewState.sink { [weak self] viewState in
guard let self else {
return
}
if let profile = viewState.members[id] {
state = .loaded(contentID: id, name: profile.displayName ?? id, avatarURL: profile.avatarURL)
cancellable = nil
}
}
}
}
}
}
extension PillContext {
enum MockType {
case loadUser
case loadedUser
}
static func mock(type: MockType) -> PillContext {
let pillType: PillType
switch type {
case .loadUser:
pillType = .user(userID: "@test:test.com")
let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
Task {
try? await Task.sleep(for: .seconds(2))
viewModel.state = .loaded(contentID: "@test:test.com", name: "Test Longer Display Text", avatarURL: URL.documentsDirectory)
}
return viewModel
case .loadedUser:
pillType = .user(userID: "@test:test.com")
let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
viewModel.state = .loaded(contentID: "@test:test.com", name: "Very Very Long Test Display Text", avatarURL: URL.documentsDirectory)
return viewModel
}
}
}
@@ -17,4 +17,27 @@
import UIKit
/// Text attachment for pills display.
final class PillTextAttachment: NSTextAttachment { }
final class PillTextAttachment: NSTextAttachment {
convenience init?(attachmentData: PillTextAttachmentData) {
let encoder = JSONEncoder()
guard let encodedData = try? encoder.encode(attachmentData) else { return nil }
self.init(data: encodedData, ofType: InfoPlistReader.main.pillsUTType)
}
var pillData: PillTextAttachmentData? {
guard let contents else {
return nil
}
let decoder = JSONDecoder()
return try? decoder.decode(PillTextAttachmentData.self, from: contents)
}
override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
var rect = super.attachmentBounds(for: textContainer, proposedLineFragment: lineFrag, glyphPosition: position, characterIndex: charIndex)
if let font = pillData?.font {
// Align the pill text vertically with the surrounding text.
rect.origin.y = font.descender + (font.lineHeight - rect.height) / 2.0
}
return rect
}
}
@@ -0,0 +1,64 @@
//
// 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
import UIKit
enum PillType: Codable {
/// A pill that mentions a user
case user(userID: String)
}
struct PillTextAttachmentData {
// MARK: - Properties
/// Pill type
let type: PillType
/// Font for the display name
let font: UIFont
}
extension PillTextAttachmentData: Codable {
// MARK: - Codable
enum CodingKeys: String, CodingKey {
case type
case font
}
enum PillTextAttachmentDataError: Error {
case noFontData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
type = try container.decode(PillType.self, forKey: .type)
let fontData = try container.decode(Data.self, forKey: .font)
if let font = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIFont.self, from: fontData) {
self.font = font
} else {
throw PillTextAttachmentDataError.noFontData
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)
let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
try container.encode(fontData, forKey: .font)
}
}
+31 -9
View File
@@ -17,21 +17,43 @@
import SwiftUI
struct PillView: View {
let imageProvider: ImageProviderProtocol?
@ObservedObject var viewModel: PillContext
/// callback triggerd by changes in the display text
let didChangeText: () -> Void
var body: some View {
Button {
MXLog.info("TEXT ATTACHMENT TEST")
} label: {
HStack {
Image(asset: Asset.Images.appLogo)
.resizable()
.scaledToFit()
}
HStack(spacing: 4) {
LoadableAvatarImage(url: viewModel.url, name: viewModel.name, contentID: viewModel.contentID, avatarSize: .custom(24), imageProvider: imageProvider)
Text(viewModel.displayText)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textOnSolidPrimary)
.lineLimit(1)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
// for now design has defined no color so we will just use gray
.background(Capsule().foregroundColor(.gray))
.frame(maxWidth: 235)
.onChange(of: viewModel.displayText) { _ in
didChangeText()
}
}
}
struct PillView_Previews: PreviewProvider, TestablePreview {
static let mockMediaProvider = MockMediaProvider()
static var loading: some View {
PillView(imageProvider: mockMediaProvider,
viewModel: PillContext.mock(type: .loadUser)) { }
}
static var previews: some View {
PillView()
loading
.previewDisplayName("Loading")
PillView(imageProvider: mockMediaProvider,
viewModel: PillContext.mock(type: .loadedUser)) { }
.previewDisplayName("Loaded Long")
}
}
@@ -16,11 +16,11 @@
import Foundation
public enum Tests {
extension ProcessInfo {
/// Flag indicating whether the app is running the unit tests.
static var isRunningUnitTests: Bool {
#if DEBUG
ProcessInfo.processInfo.environment["IS_RUNNING_UNIT_TESTS"] == "1"
processInfo.environment["IS_RUNNING_UNIT_TESTS"] == "1"
#else
false
#endif
@@ -29,7 +29,7 @@ public enum Tests {
/// Flag indicating whether the app is running the UI tests.
static var isRunningUITests: Bool {
#if DEBUG
ProcessInfo.processInfo.environment["UI_TESTS_SCREEN"] != nil
processInfo.environment["UI_TESTS_SCREEN"] != nil
#else
false
#endif
@@ -41,9 +41,9 @@ public enum Tests {
}
/// The identifier of the screen to be loaded when running UI tests.
static var screenID: UITestsScreenIdentifier? {
static var testScreenID: UITestsScreenIdentifier? {
#if DEBUG
ProcessInfo.processInfo.environment["UI_TESTS_SCREEN"].flatMap(UITestsScreenIdentifier.init)
processInfo.environment["UI_TESTS_SCREEN"].flatMap(UITestsScreenIdentifier.init)
#else
nil
#endif
@@ -55,9 +55,17 @@ public enum Tests {
}
#if DEBUG
return ProcessInfo.processInfo.environment["UI_TESTS_DISABLE_TIMELINE_ACCESSIBILITY"] != nil
return processInfo.environment["UI_TESTS_DISABLE_TIMELINE_ACCESSIBILITY"] != nil
#else
return false
#endif
}
static var isXcodePreview: Bool {
#if DEBUG
processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
#else
false
#endif
}
}
@@ -23,13 +23,13 @@ public extension Animation {
/// `noAnimation` if running tests, otherwise `default` animation if `UIAccessibility.isReduceMotionEnabled` is false
static var elementDefault: Animation {
let animation: Animation = Tests.isRunningTests ? .noAnimation : .default
let animation: Animation = ProcessInfo.isRunningTests ? .noAnimation : .default
return animation.disabledIfReduceMotionEnabled()
}
// `noAnimation` if running tests, otherwise `self` if `UIAccessibility.isReduceMotionEnabled` is false
func disabledDuringTests() -> Self {
let animation: Animation = Tests.isRunningTests ? .noAnimation : self
let animation: Animation = ProcessInfo.isRunningTests ? .noAnimation : self
return animation.disabledIfReduceMotionEnabled()
}
@@ -78,7 +78,8 @@ extension View {
struct ShimmerOverlay_Previews: PreviewProvider, TestablePreview {
static let viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: ""),
mediaProvider: MockMediaProvider()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
@@ -338,7 +338,7 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview {
mediaProvider: MockMediaProvider())
return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
@@ -154,7 +154,7 @@ struct HomeScreenEmptyStateView_Previews: PreviewProvider, TestablePreview {
mediaProvider: MockMediaProvider())
return HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
@@ -188,7 +188,8 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
mediaProvider: MockMediaProvider())
let viewModel = HomeScreenViewModel(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)),
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
@@ -201,7 +201,7 @@ struct FormattedBodyText_Previews: PreviewProvider, TestablePreview {
"<p>This is a list</p>\n<ul>\n<li>One</li>\n<li>Two</li>\n<li>And number 3</li>\n</ul>\n"
]
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL)
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled))
ScrollView {
VStack(alignment: .leading, spacing: 24.0) {
@@ -234,6 +234,7 @@ private struct PreviewBubbleModifier: ViewModifier {
.padding(timelineStyle == .bubbles ? 8 : 0)
.background(timelineStyle == .bubbles ? Color.compound._bgBubbleOutgoing : nil)
.cornerRadius(timelineStyle == .bubbles ? 12 : 0)
.environmentObject(RoomScreenViewModel.mock.context)
}
}
@@ -104,7 +104,7 @@ class TimelineTableViewController: UIViewController {
// Prevents XCUITest from invoking the diffable dataSource's cellProvider
// for each possible cell, causing layout issues
tableView.accessibilityElementsHidden = Tests.shouldDisableTimelineAccessibility
tableView.accessibilityElementsHidden = ProcessInfo.shouldDisableTimelineAccessibility
scrollToBottomPublisher
.sink { [weak self] _ in
@@ -50,6 +50,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var readReceiptsEnabled: Bool { get set }
var swiftUITimelineEnabled: Bool { get set }
var voiceMessageEnabled: Bool { get set }
var mentionsEnabled: Bool { get set }
}
extension AppSettings: DeveloperOptionsProtocol { }
@@ -45,6 +45,11 @@ struct DeveloperOptionsScreen: View {
Text("SwiftUI Timeline")
Text("Resets on reboot")
}
Toggle(isOn: $context.mentionsEnabled) {
Text("Show user mentions")
Text("Requires app reboot")
}
}
Section("Room creation") {
@@ -443,7 +443,8 @@ class ClientProxy: ClientProxyProtocol {
.finish()
let roomListService = syncService.roomListService()
let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .homeDirectory))
let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL,
mentionBuilder: MentionBuilder(mentionsEnabled: appSettings.mentionsEnabled)))
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID),
messageEventStringBuilder: roomMessageEventStringBuilder)
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
@@ -97,7 +97,9 @@ enum RoomTimelineItemFixtures {
isThreaded: false,
sender: .init(id: "", displayName: "Helena"),
content: .init(body: "",
formattedBody: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL).fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳")))
formattedBody: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled))
.fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳")))
]
/// A small chunk of events, containing 2 text items.
@@ -50,7 +50,7 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
delegate.window??.layer.speed = 0
}
guard let screenID = Tests.screenID else { fatalError("Unable to launch with unknown screen.") }
guard let screenID = ProcessInfo.testScreenID else { fatalError("Unable to launch with unknown screen.") }
let mockScreen = MockScreen(id: screenID)
navigationRootCoordinator.setRootCoordinator(mockScreen.coordinator)
@@ -162,7 +162,7 @@ class MockScreen: Identifiable {
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),
mediaProvider: MockMediaProvider())
let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)),
bugReportService: BugReportServiceMock(),
navigationStackCoordinator: navigationStackCoordinator,
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher()))