mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 03:48:27 +00:00
Run SwiftFormat.
This commit is contained in:
@@ -75,13 +75,13 @@ enum A11yIdentifiers {
|
||||
let changePIN = "app_lock_setup_settings-change_pin"
|
||||
let removePIN = "app_lock_setup_settings-remove_pin"
|
||||
}
|
||||
|
||||
|
||||
struct AnalyticsPromptScreen {
|
||||
let title = "analytics_prompt-title"
|
||||
let enable = "analytics_prompt-enable"
|
||||
let notNow = "analytics_prompt-not_now"
|
||||
}
|
||||
|
||||
|
||||
struct BugReportScreen {
|
||||
let report = "bug_report-report"
|
||||
let sendLogs = "bug_report-send_logs"
|
||||
@@ -164,9 +164,9 @@ enum A11yIdentifiers {
|
||||
|
||||
let messageComposer = "room-message_composer"
|
||||
let sendButton = "room-send_button"
|
||||
|
||||
|
||||
let composerToolbar = ComposerToolbar()
|
||||
|
||||
|
||||
struct ComposerToolbar {
|
||||
let bold = "composer_toolbar-bold"
|
||||
let italic = "composer_toolbar-italic"
|
||||
@@ -292,13 +292,13 @@ enum A11yIdentifiers {
|
||||
let mediaPicker = "create_room-media_picker"
|
||||
let filePicker = "create_room-file_picker"
|
||||
}
|
||||
|
||||
|
||||
struct PollFormScreen {
|
||||
let addOption = "poll_form-add_option"
|
||||
let pollKind = "poll_form-kind"
|
||||
let question = "poll_form-question"
|
||||
let submit = "poll_form-submit"
|
||||
|
||||
|
||||
private let optionPrefix = "poll_form-option"
|
||||
func optionID(_ index: Int) -> String {
|
||||
"\(optionPrefix)-\(index)"
|
||||
|
||||
@@ -15,7 +15,7 @@ enum Avatars {
|
||||
case room(on: RoomAvatarSizeOnScreen)
|
||||
/// custom
|
||||
case custom(CGFloat)
|
||||
|
||||
|
||||
/// Value in UIKit points
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
@@ -27,7 +27,7 @@ enum Avatars {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Value in pixels by using the scale of the main screen
|
||||
var scaledValue: CGFloat {
|
||||
value * UIScreen.main.scale
|
||||
@@ -87,7 +87,7 @@ enum UserAvatarSizeOnScreen {
|
||||
case threadSummary
|
||||
case map
|
||||
case classicAppAccount
|
||||
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
case .readReceipt:
|
||||
@@ -143,7 +143,7 @@ enum RoomAvatarSizeOnScreen {
|
||||
case spaceAddRoomsSelected
|
||||
case completionSuggestions
|
||||
case createRoomSelectSpace
|
||||
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
case .notificationSettings:
|
||||
|
||||
@@ -27,16 +27,16 @@ import UIKit
|
||||
public extension UIImage {
|
||||
convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
||||
guard blurHash.count >= 6 else { return nil }
|
||||
|
||||
|
||||
let sizeFlag = String(blurHash[0]).decode83()
|
||||
let numY = (sizeFlag / 9) + 1
|
||||
let numX = (sizeFlag % 9) + 1
|
||||
|
||||
|
||||
let quantisedMaximumValue = String(blurHash[1]).decode83()
|
||||
let maximumValue = Float(quantisedMaximumValue + 1) / 166
|
||||
|
||||
|
||||
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
|
||||
|
||||
|
||||
let colours: [(Float, Float, Float)] = (0..<numX * numY).map { i in
|
||||
if i == 0 {
|
||||
let value = String(blurHash[2..<6]).decode83()
|
||||
@@ -46,20 +46,20 @@ public extension UIImage {
|
||||
return decodeAC(value, maximumValue: maximumValue * punch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let width = Int(size.width)
|
||||
let height = Int(size.height)
|
||||
let bytesPerRow = width * 3
|
||||
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
|
||||
CFDataSetLength(data, bytesPerRow * height)
|
||||
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
|
||||
|
||||
|
||||
for y in 0..<height {
|
||||
for x in 0..<width {
|
||||
var r: Float = 0
|
||||
var g: Float = 0
|
||||
var b: Float = 0
|
||||
|
||||
|
||||
for j in 0..<numY {
|
||||
for i in 0..<numX {
|
||||
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
|
||||
@@ -69,23 +69,23 @@ public extension UIImage {
|
||||
b += colour.2 * basis
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let intR = UInt8(linearTosRGB(r))
|
||||
let intG = UInt8(linearTosRGB(g))
|
||||
let intB = UInt8(linearTosRGB(b))
|
||||
|
||||
|
||||
pixels[3 * x + 0 + y * bytesPerRow] = intR
|
||||
pixels[3 * x + 1 + y * bytesPerRow] = intG
|
||||
pixels[3 * x + 2 + y * bytesPerRow] = intB
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||
|
||||
|
||||
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
|
||||
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
|
||||
|
||||
|
||||
self.init(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float
|
||||
let quantR = value / (19 * 19)
|
||||
let quantG = (value / 19) % 19
|
||||
let quantB = value % 19
|
||||
|
||||
|
||||
return (signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
|
||||
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
|
||||
signPow((Float(quantB) - 9) / 9, 2) * maximumValue)
|
||||
@@ -149,7 +149,7 @@ private extension String {
|
||||
subscript(offset: Int) -> Character {
|
||||
self[index(startIndex, offsetBy: offset)]
|
||||
}
|
||||
|
||||
|
||||
subscript(bounds: CountableRange<Int>) -> Substring {
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||
|
||||
@@ -59,7 +59,7 @@ struct CollapsibleReactionLayout: Layout {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Calculate the layout of the rows with the reactions button and add more button
|
||||
let reactionsAndAddMore = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviewsByType.reactions + [subviewsByType.addMoreButton]))
|
||||
// If we have extended beyond the defined number of rows we are showing the expand/collapse ui
|
||||
|
||||
@@ -20,9 +20,9 @@ import Foundation
|
||||
value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var value: Value?
|
||||
|
||||
|
||||
init(value: Value? = nil) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ actor ExpiringTaskRunner<T: Sendable> {
|
||||
private var continuation: CheckedContinuation<T, Error>?
|
||||
|
||||
private var task: @Sendable () async throws -> T
|
||||
|
||||
|
||||
init(_ task: @escaping @Sendable () async throws -> T) {
|
||||
self.task = task
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ private extension [Bit] {
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
mutating func takeBits16(_ count: Int) throws(BitError) -> UInt16 {
|
||||
guard count <= 16 else { throw .moreThan16BitsTaken }
|
||||
|
||||
@@ -92,7 +92,7 @@ private extension [Bit] {
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
mutating func takeUInt8() throws(BitError) -> UInt8 {
|
||||
try takeBits(8)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ extension View {
|
||||
})
|
||||
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions, message: message)
|
||||
}
|
||||
|
||||
|
||||
// periphery: ignore - not used yet but might be useful
|
||||
func alert<Item: AlertProtocol, Actions: View>(item: Binding<Item?>, @ViewBuilder actions: (Item) -> Actions) -> some View {
|
||||
let binding = Binding<Bool>(get: {
|
||||
@@ -51,7 +51,7 @@ struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
|
||||
var role: ButtonRole?
|
||||
let action: (() -> Void)?
|
||||
}
|
||||
|
||||
|
||||
struct AlertTextField: Identifiable {
|
||||
let id = UUID()
|
||||
let placeholder: String
|
||||
@@ -59,7 +59,7 @@ struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
|
||||
let autoCapitalization: TextInputAutocapitalization
|
||||
let autoCorrectionDisabled: Bool
|
||||
}
|
||||
|
||||
|
||||
/// An identifier that can be used to distinguish one error from another.
|
||||
let id: T
|
||||
/// The alert's title.
|
||||
@@ -86,7 +86,7 @@ extension AlertInfo {
|
||||
title = L10n.commonError
|
||||
message = L10n.errorUnknown
|
||||
}
|
||||
|
||||
|
||||
// periphery: ignore - might be useful in the future
|
||||
/// Initialises the type with the title from an `Error`'s localised description along with the default Ok button.
|
||||
///
|
||||
@@ -109,7 +109,7 @@ extension View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let textFields = item.textFields {
|
||||
VStack(spacing: 24) {
|
||||
ForEach(textFields) { textField in
|
||||
@@ -119,12 +119,12 @@ extension View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button(item.primaryButton.title, role: item.primaryButton.role) {
|
||||
item.primaryButton.action?()
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.alertInfo.primaryButton)
|
||||
|
||||
|
||||
if let secondaryButton = item.secondaryButton {
|
||||
Button(secondaryButton.title, role: secondaryButton.role) {
|
||||
secondaryButton.action?()
|
||||
|
||||
@@ -36,7 +36,7 @@ extension AttributedString {
|
||||
case (false, false):
|
||||
.plainText
|
||||
}
|
||||
|
||||
|
||||
components.append(AttributedStringBuilderComponent(id: String(attributedString.characters),
|
||||
attributedString: attributedString,
|
||||
type: componentType))
|
||||
|
||||
@@ -28,9 +28,9 @@ public extension Bundle {
|
||||
/// Overrides `Bundle.app.preferredLocalizations` for testing translations since this is
|
||||
/// only for testing, and is changed at runtime only in tests, it's fine to keep as `nonisolated(unsafe)`
|
||||
nonisolated(unsafe) static var overrideLocalizations: [String]?
|
||||
|
||||
|
||||
private static let cachedBundles = Mutex<[String: Bundle]>([:])
|
||||
|
||||
|
||||
/// Get an lproj language bundle from the receiver bundle.
|
||||
/// - Parameter language: The language to try to load.
|
||||
/// - Returns: The lproj bundle if found otherwise nil.
|
||||
@@ -38,24 +38,24 @@ public extension Bundle {
|
||||
if let bundle = cachedValue(forKey: language) {
|
||||
return bundle
|
||||
}
|
||||
|
||||
|
||||
guard let lprojURL = Bundle.app.url(forResource: language, withExtension: "lproj") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let bundle = Bundle(url: lprojURL)
|
||||
|
||||
|
||||
cacheValue(bundle, forKey: language)
|
||||
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private static func cacheValue(_ value: Bundle?, forKey key: String) {
|
||||
cachedBundles.withLock { $0[key] = value }
|
||||
}
|
||||
|
||||
|
||||
private static func cachedValue(forKey key: String) -> Bundle? {
|
||||
cachedBundles.withLock { $0[key] }
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ extension ClientBuilder {
|
||||
.enableShareHistoryOnInvite(enableShareHistoryOnInvite: true)
|
||||
.autoEnableBackups(autoEnableBackups: true)
|
||||
}
|
||||
|
||||
|
||||
// Set recipient strategy and trust requirement even if `setupEncryption` is false to ensure messages
|
||||
// from insecure devices aren't displayed in push notifications.
|
||||
// See https://github.com/element-hq/element-x-ios/issues/4702.
|
||||
|
||||
@@ -25,7 +25,7 @@ extension View {
|
||||
})
|
||||
return confirmationDialog(item.wrappedValue?.title ?? "", isPresented: binding, titleVisibility: titleVisibility, presenting: item.wrappedValue, actions: actions)
|
||||
}
|
||||
|
||||
|
||||
// periphery: ignore - not used yet but might be useful
|
||||
func confirmationDialog<Item: ConfirmationDialogProtocol, Actions: View, Message: View>(item: Binding<Item?>,
|
||||
titleVisibility: Visibility = .automatic,
|
||||
|
||||
@@ -18,7 +18,7 @@ extension Encodable {
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
|
||||
func toJsonString(_ encoder: JSONEncoder = JSONEncoder()) throws -> String? {
|
||||
try toJsonDictionary(encoder).jsonString
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ extension FileManager {
|
||||
}
|
||||
return isDirectory.boolValue
|
||||
}
|
||||
|
||||
|
||||
func createDirectoryIfNeeded(at url: URL, withIntermediateDirectories: Bool = true) throws {
|
||||
guard !directoryExists(at: url) else {
|
||||
return
|
||||
@@ -36,7 +36,7 @@ extension FileManager {
|
||||
|
||||
return newURL
|
||||
}
|
||||
|
||||
|
||||
@discardableResult
|
||||
func writeDataToTemporaryDirectory(data: Data, fileName: String, withinAppGroupContainer: Bool = false) throws -> URL {
|
||||
let baseURL: URL = withinAppGroupContainer ? .appGroupTemporaryDirectory : .temporaryDirectory
|
||||
@@ -59,7 +59,7 @@ extension FileManager {
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
|
||||
func sizeForDirectory(at url: URL) throws -> UInt {
|
||||
guard let enumerator = enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey]) else {
|
||||
throw FileManagerError.invalidFileSize
|
||||
|
||||
@@ -16,7 +16,7 @@ extension ImageCache {
|
||||
result.diskStorage.config.sizeLimit = 1
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
static var onlyOnDisk: ImageCache {
|
||||
let result = ImageCache.default
|
||||
result.memoryStorage.config.totalCostLimit = 1
|
||||
|
||||
@@ -133,7 +133,7 @@ extension NSItemProvider {
|
||||
guard registeredContentTypes.contains(where: { $0.conforms(to: .fileURL) }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
for type in registeredContentTypes {
|
||||
if let fileExtension = type.preferredFilenameExtension {
|
||||
return .init(type: type, fileExtension: fileExtension)
|
||||
|
||||
@@ -17,7 +17,7 @@ extension ProcessInfo {
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/// Flag indicating whether the app is running the UI tests.
|
||||
static var isRunningUITests: Bool {
|
||||
#if DEBUG
|
||||
@@ -42,7 +42,7 @@ extension ProcessInfo {
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/// Flag indicating whether the app is running the UI tests or unit tests.
|
||||
static var isRunningTests: Bool {
|
||||
isRunningUITests || isRunningUnitTests || isRunningIntegrationTests || isRunningAccessibilityTests
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
|
||||
struct SnapshotPrecisionPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: Float = 1.0
|
||||
|
||||
|
||||
static func reduce(value: inout Float, nextValue: () -> Float) {
|
||||
value = nextValue()
|
||||
}
|
||||
@@ -19,7 +19,7 @@ struct SnapshotPrecisionPreferenceKey: PreferenceKey {
|
||||
|
||||
struct SnapshotPerceptualPrecisionPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: Float = 0.98
|
||||
|
||||
|
||||
static func reduce(value: inout Float, nextValue: () -> Float) {
|
||||
value = nextValue()
|
||||
}
|
||||
@@ -27,7 +27,7 @@ struct SnapshotPerceptualPrecisionPreferenceKey: PreferenceKey {
|
||||
|
||||
struct SnapshotFulfillmentPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: Wrapper?
|
||||
|
||||
|
||||
static func reduce(value: inout Wrapper?, nextValue: () -> Wrapper?) {
|
||||
value = nextValue()
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public extension Task where Success == Never, Failure == Never {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Dispatches the given throwing closure onto the given queue, wrapped within
|
||||
/// a continuation to make it non-blocking and awaitable.
|
||||
///
|
||||
@@ -59,7 +59,7 @@ extension Task {
|
||||
func store(in cancellables: inout Set<AnyCancellable>) {
|
||||
asCancellable().store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
func asCancellable() -> AnyCancellable {
|
||||
AnyCancellable(cancel)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ extension UIDevice {
|
||||
var isPhone: Bool {
|
||||
userInterfaceIdiom == .phone
|
||||
}
|
||||
|
||||
|
||||
var initialDeviceName: String {
|
||||
L10n.loginInitialDeviceNameIos(InfoPlistReader.main.bundleDisplayName)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ extension UNNotificationContent {
|
||||
@objc var receiverID: String? {
|
||||
userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String
|
||||
}
|
||||
|
||||
|
||||
@objc var roomID: String? {
|
||||
userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String
|
||||
}
|
||||
@@ -21,7 +21,7 @@ extension UNNotificationContent {
|
||||
@objc var eventID: String? {
|
||||
userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String
|
||||
}
|
||||
|
||||
|
||||
@objc var pusherNotificationClientIdentifier: String? {
|
||||
userInfo[NotificationConstants.UserInfoKey.pusherNotificationClientIdentifier] as? String
|
||||
}
|
||||
|
||||
@@ -31,13 +31,13 @@ extension URL {
|
||||
.appending(component: "Logs", directoryHint: .isDirectory)
|
||||
.appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory)
|
||||
}
|
||||
|
||||
|
||||
/// The base directory where all session data is stored.
|
||||
static var sessionsBaseDirectory: URL {
|
||||
let applicationSupportSessionsURL = applicationSupportBaseDirectory.appending(component: "Sessions", directoryHint: .isDirectory)
|
||||
|
||||
try? FileManager.default.createDirectoryIfNeeded(at: applicationSupportSessionsURL)
|
||||
|
||||
|
||||
return applicationSupportSessionsURL
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ extension URL {
|
||||
.appending(component: "Library", directoryHint: .isDirectory)
|
||||
.appending(component: "Application Support", directoryHint: .isDirectory)
|
||||
.appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory)
|
||||
|
||||
|
||||
try? FileManager.default.createDirectoryIfNeeded(at: url)
|
||||
|
||||
do {
|
||||
@@ -57,7 +57,7 @@ extension URL {
|
||||
} catch {
|
||||
MXLog.error("Failed excluding Application Support from backups")
|
||||
}
|
||||
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -68,12 +68,12 @@ extension URL {
|
||||
.appending(component: "Caches", directoryHint: .isDirectory)
|
||||
.appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory)
|
||||
.appending(component: "Sessions", directoryHint: .isDirectory)
|
||||
|
||||
|
||||
try? FileManager.default.createDirectoryIfNeeded(at: url)
|
||||
|
||||
// Caches are excluded from backups automatically.
|
||||
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
|
||||
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -84,12 +84,12 @@ extension URL {
|
||||
static var appGroupTemporaryDirectory: URL {
|
||||
let url = appGroupContainerDirectory
|
||||
.appending(component: "tmp", directoryHint: .isDirectory)
|
||||
|
||||
|
||||
try? FileManager.default.createDirectoryIfNeeded(at: url)
|
||||
|
||||
// Temporary files are excluded from backups automatically.
|
||||
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
|
||||
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ extension URL {
|
||||
|
||||
MXLog.info("Found \(String(describing: proxies?.count)) proxies, using the first one.")
|
||||
MXLog.info("Proxy type is \(proxyType)")
|
||||
|
||||
|
||||
guard let host = firstProxy[kCFProxyHostNameKey] as? String else {
|
||||
MXLog.error("Found proxy with invalid host name")
|
||||
return nil
|
||||
@@ -138,23 +138,23 @@ extension URL {
|
||||
static var mockMXCAudio: URL {
|
||||
"mxc://matrix.org/1234567890AuDiO"
|
||||
}
|
||||
|
||||
|
||||
static var mockMXCFile: URL {
|
||||
"mxc://matrix.org/1234567890FiLe"
|
||||
}
|
||||
|
||||
|
||||
static var mockMXCImage: URL {
|
||||
"mxc://matrix.org/1234567890ImAgE"
|
||||
}
|
||||
|
||||
|
||||
static var mockMXCVideo: URL {
|
||||
"mxc://matrix.org/1234567890ViDeO"
|
||||
}
|
||||
|
||||
|
||||
static var mockMXCAvatar: URL {
|
||||
"mxc://matrix.org/1234567890AvAtAr"
|
||||
}
|
||||
|
||||
|
||||
static var mockMXCUserAvatar: URL {
|
||||
"mxc://matrix.org/1234567890AvAtArUsEr"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
|
||||
private static let attributeMSC4286 = "msc4286-external-payment-details"
|
||||
private static let caches = Mutex<[String: LRUCache<String, AttributedString>]>([:])
|
||||
|
||||
|
||||
static func invalidateCaches() {
|
||||
caches.withLock { $0.removeAll() }
|
||||
}
|
||||
@@ -53,7 +53,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
self.cacheKey = cacheKey
|
||||
self.mentionBuilder = mentionBuilder
|
||||
}
|
||||
|
||||
|
||||
func fromPlain(_ string: String?) -> AttributedString? {
|
||||
guard let string else {
|
||||
return nil
|
||||
@@ -62,7 +62,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
if let cached = Self.cachedValue(forKey: string, cacheKey: cacheKey) {
|
||||
return cached
|
||||
}
|
||||
|
||||
|
||||
let mutableAttributedString = NSMutableAttributedString(string: string)
|
||||
addLinksAndMentions(mutableAttributedString)
|
||||
addMatrixEntityPermalinkAttributesTo(mutableAttributedString)
|
||||
@@ -72,7 +72,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/// Do not use the default HTML renderer of NSAttributedString because this method
|
||||
/// runs on the UI thread which we want to avoid because renderHTMLString is called
|
||||
/// most of the time from a background thread.
|
||||
@@ -84,7 +84,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
if let cached = Self.cachedValue(forKey: originalHTMLString, cacheKey: cacheKey) {
|
||||
return cached
|
||||
}
|
||||
|
||||
|
||||
let htmlString = originalHTMLString.replacingHtmlBreaksOccurrences()
|
||||
|
||||
let doc = try? SwiftSoup.parseBodyFragment(htmlString)
|
||||
@@ -105,7 +105,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
@@ -133,7 +133,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
if (node.previousSibling() as? Element)?.tagName() == "br" {
|
||||
text.trimPrefix(" ")
|
||||
}
|
||||
|
||||
|
||||
result.append(NSAttributedString(string: text))
|
||||
continue
|
||||
}
|
||||
@@ -155,7 +155,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
content = attributedString(element: childElement, documentBody: documentBody, preserveFormatting: preserveFormatting, listTag: listTag, listIndex: &childIndex, indentLevel: indentLevel)
|
||||
content.append(NSAttributedString(string: "\n"))
|
||||
content.setFontPreservingSymbolicTraits(UIFont.boldSystemFont(ofSize: size))
|
||||
|
||||
|
||||
case "p", "div":
|
||||
content = attributedString(element: childElement, documentBody: documentBody, preserveFormatting: preserveFormatting, listTag: listTag, listIndex: &childIndex, indentLevel: indentLevel)
|
||||
content.append(NSAttributedString(string: "\n"))
|
||||
@@ -245,7 +245,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
if indentLevel > 0 || !element.ownText().isEmpty {
|
||||
content.insert(NSAttributedString("\n"), at: 0)
|
||||
}
|
||||
|
||||
|
||||
case "li":
|
||||
var bullet = String(repeating: " ", count: indentLevel)
|
||||
if listTag == "ol" {
|
||||
@@ -283,11 +283,11 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
if caches[cacheKey] == nil {
|
||||
caches[cacheKey] = LRUCache<String, AttributedString>(countLimit: 1000)
|
||||
}
|
||||
|
||||
|
||||
caches[cacheKey]?.setValue(value, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func cachedValue(forKey key: String, cacheKey: String) -> AttributedString? {
|
||||
caches.withLock { $0[cacheKey]?.value(forKey: key) }
|
||||
}
|
||||
@@ -305,7 +305,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
|
||||
let identifier = String(string[matchRange])
|
||||
|
||||
|
||||
return TextParsingMatch(type: .userID(identifier: identifier), range: match.range)
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
if hasLink {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch match.type {
|
||||
case .atRoom:
|
||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||
@@ -420,7 +420,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func detectPhishingAttempts(_ attributedString: NSMutableAttributedString) {
|
||||
attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
|
||||
guard value != nil, let internalURL = value as? URL else {
|
||||
|
||||
@@ -14,7 +14,7 @@ struct AttributedStringBuilderComponent: Hashable, Identifiable {
|
||||
case blockquote
|
||||
case codeBlock
|
||||
}
|
||||
|
||||
|
||||
/// Identifier for the `Identifiable` conformance, allows edits to the `FormattedBodyText` to animate seamlessly
|
||||
let id: String
|
||||
let attributedString: AttributedString
|
||||
|
||||
@@ -15,7 +15,7 @@ enum PhishingDetector {
|
||||
let disambiguatedDisplayString = displayString.replacingOccurrences(of: "﹒", with: ".")
|
||||
let linkMatch = MatrixEntityRegex.linkRegex.firstMatch(in: disambiguatedDisplayString)
|
||||
let linkMatchLength = linkMatch?.range.length ?? 0
|
||||
|
||||
|
||||
// We check if we the link string contains a matrix user ID.
|
||||
if let match = MatrixEntityRegex.userIdentifierRegex.firstMatch(in: disambiguatedDisplayString),
|
||||
// If there is a bigger permalink including it we leave it handled by the link branch
|
||||
|
||||
@@ -33,26 +33,26 @@ struct InfoPlistReader {
|
||||
private enum Values {
|
||||
static let mentionPills = "Mention Pills"
|
||||
}
|
||||
|
||||
|
||||
/// Info.plist reader on the bundle object that contains the current executable.
|
||||
static let main = InfoPlistReader(bundle: .main)
|
||||
|
||||
|
||||
/// Info.plist reader on the bundle object that contains the main app executable.
|
||||
static let app = InfoPlistReader(bundle: .app)
|
||||
|
||||
|
||||
private let bundle: Bundle
|
||||
|
||||
|
||||
/// Initializer
|
||||
/// - Parameter bundle: bundle to read values from
|
||||
init(bundle: Bundle) {
|
||||
self.bundle = bundle
|
||||
}
|
||||
|
||||
|
||||
/// App group identifier set in Info.plist of the target
|
||||
var appGroupIdentifier: String {
|
||||
infoPlistValue(forKey: Keys.appGroupIdentifier)
|
||||
}
|
||||
|
||||
|
||||
/// Base bundle identifier set in Info.plist of the target
|
||||
var baseBundleIdentifier: String {
|
||||
infoPlistValue(forKey: Keys.baseBundleIdentifier)
|
||||
@@ -62,27 +62,27 @@ struct InfoPlistReader {
|
||||
var keychainAccessGroupIdentifier: String {
|
||||
infoPlistValue(forKey: Keys.keychainAccessGroupIdentifier)
|
||||
}
|
||||
|
||||
|
||||
/// Bundle executable of the target
|
||||
var bundleExecutable: String {
|
||||
infoPlistValue(forKey: kCFBundleExecutableKey as String)
|
||||
}
|
||||
|
||||
|
||||
/// Bundle identifier of the target
|
||||
var bundleIdentifier: String {
|
||||
infoPlistValue(forKey: kCFBundleIdentifierKey as String)
|
||||
}
|
||||
|
||||
|
||||
/// Bundle short version string of the target
|
||||
var bundleShortVersionString: String {
|
||||
infoPlistValue(forKey: Keys.bundleShortVersion)
|
||||
}
|
||||
|
||||
|
||||
/// Bundle version of the target
|
||||
var bundleVersion: String {
|
||||
infoPlistValue(forKey: kCFBundleVersionKey as String)
|
||||
}
|
||||
|
||||
|
||||
/// Bundle display name of the target
|
||||
var bundleDisplayName: String {
|
||||
infoPlistValue(forKey: Keys.bundleDisplayName)
|
||||
@@ -104,7 +104,7 @@ struct InfoPlistReader {
|
||||
}
|
||||
|
||||
// MARK: - Mention Pills
|
||||
|
||||
|
||||
/// Mention Pills UTType
|
||||
var pillsUTType: String {
|
||||
let exportedTypes: [[String: Any]] = infoPlistValue(forKey: Keys.utExportedTypeDeclarationsKey)
|
||||
|
||||
@@ -20,7 +20,7 @@ enum ExtensionLogger {
|
||||
formatter.minimumFractionDigits = 2
|
||||
return formatter
|
||||
}
|
||||
|
||||
|
||||
private static var formattedMemoryAvailable: String {
|
||||
let freeBytes = os_proc_available_memory()
|
||||
let freeMB = Double(freeBytes) / 1024 / 1024
|
||||
@@ -29,7 +29,7 @@ enum ExtensionLogger {
|
||||
}
|
||||
return "\(formattedStr) MB"
|
||||
}
|
||||
|
||||
|
||||
/// Details: https://developer.apple.com/forums/thread/105088
|
||||
/// - Returns: Current memory footprint
|
||||
private static var memoryFootprint: Float? {
|
||||
@@ -50,10 +50,10 @@ enum ExtensionLogger {
|
||||
guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return Float(info.phys_footprint)
|
||||
}
|
||||
|
||||
|
||||
/// Formatted memory footprint for debugging purposes
|
||||
/// - Returns: Memory footprint in MBs as a readable string
|
||||
static var formattedMemoryFootprint: String {
|
||||
@@ -64,7 +64,7 @@ enum ExtensionLogger {
|
||||
}
|
||||
return "\(formattedStr) MB"
|
||||
}
|
||||
|
||||
|
||||
static func logMemory(with tag: String) {
|
||||
MXLog.info("\(tag) Memory: footprint: \(formattedMemoryFootprint) - available: \(formattedMemoryAvailable)")
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
|
||||
/// Map annotations
|
||||
let annotations: [LocationAnnotation]
|
||||
|
||||
|
||||
init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [LocationAnnotation] = []) {
|
||||
self.zoomLevel = zoomLevel
|
||||
self.initialZoomLevel = initialZoomLevel
|
||||
@@ -36,7 +36,7 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let mapURLBuilder: MapTilerURLBuilderProtocol
|
||||
|
||||
|
||||
let options: Options
|
||||
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
@@ -93,7 +93,7 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private func setupMap(mapView: MLNMapView, with options: Options) {
|
||||
mapView.addAnnotations(options.annotations)
|
||||
mapView.zoomLevel = options.annotations.isEmpty ? options.initialZoomLevel : options.zoomLevel
|
||||
@@ -174,16 +174,16 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
extension MapLibreMapView {
|
||||
class Coordinator: NSObject, MLNMapViewDelegate {
|
||||
// MARK: - Properties
|
||||
|
||||
|
||||
var mapLibreView: MapLibreMapView
|
||||
|
||||
private var previousUserLocation: MLNUserLocation?
|
||||
/// Tracks the last center coordinate reported by the map (or set programmatically),
|
||||
/// so that `updateUIView` can tell apart external binding changes from internal ones.
|
||||
var lastReportedCenter: CLLocationCoordinate2D?
|
||||
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
|
||||
init(_ mapLibreView: MapLibreMapView) {
|
||||
self.mapLibreView = mapLibreView
|
||||
}
|
||||
@@ -212,7 +212,7 @@ extension MapLibreMapView {
|
||||
mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.options.zoomLevel, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
previousUserLocation = userLocation
|
||||
updateGeolocationUncertainty(location: userLocation)
|
||||
}
|
||||
@@ -238,7 +238,7 @@ extension MapLibreMapView {
|
||||
mapLibreView.mapCenterCoordinate = center
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func mapView(_ mapView: MLNMapView, shouldChangeFrom oldCamera: MLNMapCamera, to newCamera: MLNMapCamera, reason: MLNCameraChangeReason) -> Bool {
|
||||
// we send the userDidPan event only for the reasons that actually will change the map center, and not zoom only / rotations only events.
|
||||
switch reason {
|
||||
@@ -259,21 +259,21 @@ extension MapLibreMapView {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// MARK: Callout
|
||||
|
||||
|
||||
func mapView(_ mapView: MLNMapView, annotationCanShowCallout annotation: MLNAnnotation) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
|
||||
private func updateGeolocationUncertainty(location: MLNUserLocation) {
|
||||
guard let clLocation = location.location, clLocation.horizontalAccuracy >= 0 else {
|
||||
mapLibreView.geolocationUncertainty = nil
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
mapLibreView.geolocationUncertainty = clLocation.horizontalAccuracy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ struct MapLibreStaticMapView<PinAnnotation: View>: View {
|
||||
private let mapTilerAttributionPlacement: MapTilerAttributionPlacement
|
||||
private let mapSize: CGSize
|
||||
private let pinAnnotationView: PinAnnotation
|
||||
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State private var fetchAttempt = 0
|
||||
|
||||
@@ -79,13 +79,13 @@ struct MapLibreStaticMapView<PinAnnotation: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var placeholderImage: some View {
|
||||
Image(asset: Asset.Images.mapBlurred)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
}
|
||||
|
||||
|
||||
private var errorView: some View {
|
||||
Button {
|
||||
fetchAttempt += 1
|
||||
|
||||
@@ -49,7 +49,7 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .allUsers, font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
return
|
||||
@@ -80,7 +80,7 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
|
||||
@@ -93,7 +93,7 @@ struct MessageText: UIViewRepresentable {
|
||||
guard let baseText = try? NSAttributedString(attributedString, including: \.elementX) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let combined = NSMutableAttributedString(attributedString: baseText)
|
||||
if trailingReservedSize.width > 0 {
|
||||
let attachment = TransparentTextAttachment()
|
||||
@@ -101,7 +101,7 @@ struct MessageText: UIViewRepresentable {
|
||||
attachment.bounds = CGRect(origin: .zero,
|
||||
size: CGSize(width: trailingReservedSize.width,
|
||||
height: max(trailingReservedSize.height, 1)))
|
||||
|
||||
|
||||
// Inherit the font from the preceding character so the appended runs
|
||||
// (newline and attachment) carry the same line metrics as the surrounding
|
||||
// text — otherwise a small default font on those characters can shrink the
|
||||
@@ -109,7 +109,7 @@ struct MessageText: UIViewRepresentable {
|
||||
let trailingFont = baseText.length > 0
|
||||
? baseText.attribute(.font, at: baseText.length - 1, effectiveRange: nil)
|
||||
: nil
|
||||
|
||||
|
||||
// When the last paragraph's natural text direction doesn't match the layout
|
||||
// direction, the inline attachment would land on the wrong side and overlap the
|
||||
// overlaid timestamp. Force it onto its own line so the bubble just grows taller.
|
||||
@@ -122,7 +122,7 @@ struct MessageText: UIViewRepresentable {
|
||||
}
|
||||
combined.append(newlineString)
|
||||
}
|
||||
|
||||
|
||||
let attachmentString = NSMutableAttributedString(attachment: attachment)
|
||||
if let trailingFont {
|
||||
attachmentString.addAttribute(.font,
|
||||
@@ -131,19 +131,19 @@ struct MessageText: UIViewRepresentable {
|
||||
}
|
||||
combined.append(attachmentString)
|
||||
}
|
||||
|
||||
|
||||
return combined
|
||||
}
|
||||
|
||||
|
||||
private func lastParagraphDirectionMatchesLayout(in attributedText: NSAttributedString) -> Bool {
|
||||
let string = attributedText.string as NSString
|
||||
guard string.length > 0 else { return true }
|
||||
|
||||
|
||||
// Isolate the last paragraph because its direction is what decides
|
||||
// which side the trailing attachment ends up on.
|
||||
let lastParagraphRange = string.paragraphRange(for: NSRange(location: string.length - 1, length: 0))
|
||||
let lastParagraph = string.substring(with: lastParagraphRange)
|
||||
|
||||
|
||||
let textIsRTL = lastParagraph.firstStrongCharacterIsRTL
|
||||
let layoutIsRTL = layoutDirection == .rightToLeft
|
||||
return textIsRTL == layoutIsRTL
|
||||
|
||||
@@ -25,7 +25,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo
|
||||
|
||||
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.
|
||||
delegate = parentView?.superview as? PillAttachmentViewProviderDelegate
|
||||
tracksTextAttachmentViewBounds = true
|
||||
@@ -34,7 +34,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo
|
||||
@MainActor
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
|
||||
guard let textAttachment = textAttachment as? PillTextAttachment,
|
||||
let pillData = textAttachment.pillData else {
|
||||
MXLog.failure("Attachment is missing data or not of expected class")
|
||||
@@ -82,7 +82,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo
|
||||
|
||||
final class ComposerMentionDisplayHelper: MentionDisplayHelper {
|
||||
weak var timelineContext: TimelineViewModel.Context?
|
||||
|
||||
|
||||
init(timelineContext: TimelineViewModel.Context) {
|
||||
self.timelineContext = timelineContext
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
|
||||
enum PillUtilities {
|
||||
static let atRoom = "@room"
|
||||
|
||||
|
||||
/// Used by the WYSIWYG as the urlString value to identify @room mentions
|
||||
static let composerAtRoomURLString = "#"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ struct PillView: View {
|
||||
var backgroundColor: Color {
|
||||
context.viewState.isOwnMention ? .compound.bgBadgeAccent : .compound.bgBadgePrimary
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.onChange(of: context.viewState.displayText) {
|
||||
|
||||
@@ -22,13 +22,13 @@ private struct ProgressMaskModifier: ViewModifier {
|
||||
private let progress: CGFloat
|
||||
private let trackColor: Color
|
||||
private let backgroundTrackColor: Color
|
||||
|
||||
|
||||
init(progress: CGFloat, trackColor: Color, backgroundTrackColor: Color) {
|
||||
self.progress = progress
|
||||
self.trackColor = trackColor
|
||||
self.backgroundTrackColor = backgroundTrackColor
|
||||
}
|
||||
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import MatrixRustSDK
|
||||
/// protocol with a specialisation for the type it listens for.
|
||||
final class SDKListener<T>: Sendable {
|
||||
private let onUpdateClosure: @Sendable (T) -> Void
|
||||
|
||||
|
||||
/// Creates a new listener.
|
||||
/// - Parameter onUpdateClosure: A closure that will be called whenever a new value is available.
|
||||
init(_ onUpdateClosure: @escaping @Sendable (T) -> Void) {
|
||||
|
||||
@@ -16,9 +16,9 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
scrollView?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var shouldScrollToTopClosure: ((UIScrollView) -> Bool)?
|
||||
|
||||
|
||||
private let didScrollSubject = PassthroughSubject<Void, Never>()
|
||||
var didScroll: AnyPublisher<Void, Never> {
|
||||
didScrollSubject.eraseToAnyPublisher()
|
||||
@@ -54,7 +54,7 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
updateDidScroll(scrollView)
|
||||
}
|
||||
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
updateDidScroll(scrollView)
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||
updateDidScroll(scrollView)
|
||||
}
|
||||
|
||||
|
||||
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||
guard let shouldScrollToTopClosure else {
|
||||
// Default behaviour
|
||||
|
||||
@@ -15,34 +15,34 @@ final class ShareToMapsAppActivity: UIActivity {
|
||||
case google
|
||||
case osm
|
||||
}
|
||||
|
||||
|
||||
private let type: MapsAppType
|
||||
private let location: CLLocationCoordinate2D
|
||||
private let senderName: String?
|
||||
|
||||
|
||||
init(type: MapsAppType, location: CLLocationCoordinate2D, senderName: String?) {
|
||||
self.type = type
|
||||
self.location = location
|
||||
self.senderName = senderName
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
override private init() {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
||||
override var activityTitle: String? {
|
||||
type.activityTitle
|
||||
}
|
||||
|
||||
|
||||
override var activityType: UIActivity.ActivityType {
|
||||
.shareToMapsApp
|
||||
}
|
||||
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
UIApplication.shared.open(type.activityURL(for: location, senderName: senderName), options: [:]) { [weak self] result in
|
||||
self?.activityDidFinish(result)
|
||||
@@ -76,7 +76,7 @@ extension ShareToMapsAppActivity.MapsAppType {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var activityTitle: String {
|
||||
switch self {
|
||||
case .apple:
|
||||
|
||||
@@ -12,21 +12,21 @@ import SwiftUI
|
||||
public extension Animation {
|
||||
/// Animation to be used to disable animations.
|
||||
static let noAnimation: Animation = .linear(duration: 0)
|
||||
|
||||
|
||||
/// `noAnimation` if running tests, otherwise `default` animation if `UIAccessibility.isReduceMotionEnabled` is false
|
||||
static var elementDefault: Animation {
|
||||
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 = ProcessInfo.isRunningTests ? .noAnimation : self
|
||||
return animation.disabledIfReduceMotionEnabled()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private func disabledIfReduceMotionEnabled() -> Self {
|
||||
UIAccessibility.isReduceMotionEnabled ? .noAnimation : self
|
||||
}
|
||||
|
||||
@@ -22,28 +22,28 @@ class StateStoreViewModel<State: BindableState, ViewAction> {
|
||||
///
|
||||
/// Left as public for `ViewModel` implementations convenience.
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
/// Constrained interface for passing to Views.
|
||||
var context: Context
|
||||
|
||||
|
||||
var state: State {
|
||||
get { context.viewState }
|
||||
set { context.viewState = newValue }
|
||||
}
|
||||
|
||||
|
||||
init(initialViewState: State, mediaProvider: MediaProviderProtocol? = nil) {
|
||||
context = Context(initialViewState: initialViewState, mediaProvider: mediaProvider)
|
||||
context.viewModel = self
|
||||
}
|
||||
|
||||
|
||||
/// Override to handles incoming `ViewAction`s from the `ViewModel`.
|
||||
/// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation.
|
||||
func process(viewAction: ViewAction) {
|
||||
// Default implementation, -no-op
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Context
|
||||
|
||||
|
||||
/// A constrained and concise interface for interacting with the ViewModel.
|
||||
///
|
||||
/// This class is closely bound to`StateStoreViewModel`. It provides the exact interface the view should need to interact
|
||||
@@ -60,26 +60,26 @@ class StateStoreViewModel<State: BindableState, ViewAction> {
|
||||
@MainActor
|
||||
final class Context: ObservableObject {
|
||||
fileprivate weak var viewModel: StateStoreViewModel?
|
||||
|
||||
|
||||
/// Get-able/Observable `Published` property for the `ViewState`
|
||||
@Published fileprivate(set) var viewState: State
|
||||
|
||||
|
||||
/// An optional image loading service so that views can manage themselves
|
||||
/// Intentionally non-generic so that it doesn't grow uncontrollably
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
|
||||
|
||||
/// Set-able/Bindable access to the bindable state.
|
||||
subscript<T>(dynamicMember keyPath: WritableKeyPath<State.BindStateType, T>) -> T {
|
||||
get { viewState.bindings[keyPath: keyPath] }
|
||||
set { viewState.bindings[keyPath: keyPath] = newValue }
|
||||
}
|
||||
|
||||
|
||||
/// Send a `ViewAction` to the `ViewModel` for processing.
|
||||
/// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`.
|
||||
func send(viewAction: ViewAction) {
|
||||
viewModel?.process(viewAction: viewAction)
|
||||
}
|
||||
|
||||
|
||||
fileprivate init(initialViewState: State, mediaProvider: MediaProviderProtocol?) {
|
||||
self.viewState = initialViewState
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
@@ -24,26 +24,26 @@ class StateStoreViewModelV2<State: BindableState, ViewAction> {
|
||||
///
|
||||
/// Left as public for `ViewModel` implementations convenience.
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
/// Constrained interface for passing to Views.
|
||||
var context: Context
|
||||
|
||||
|
||||
var state: State {
|
||||
get { context.viewState }
|
||||
set { context.viewState = newValue }
|
||||
}
|
||||
|
||||
|
||||
init(initialViewState: State, mediaProvider: MediaProviderProtocol? = nil) {
|
||||
context = Context(initialViewState: initialViewState, mediaProvider: mediaProvider)
|
||||
context.viewModel = self
|
||||
}
|
||||
|
||||
|
||||
/// Override to handles incoming `ViewAction`s from the `ViewModel`.
|
||||
/// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation.
|
||||
func process(viewAction: ViewAction) {
|
||||
// Default implementation, -no-op
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Context
|
||||
|
||||
/// A constrained and concise interface for interacting with the ViewModel.
|
||||
@@ -62,26 +62,26 @@ class StateStoreViewModelV2<State: BindableState, ViewAction> {
|
||||
@MainActor
|
||||
@Observable final class Context {
|
||||
fileprivate weak var viewModel: StateStoreViewModelV2?
|
||||
|
||||
|
||||
/// Get-able property for the `ViewState`
|
||||
fileprivate(set) var viewState: State
|
||||
|
||||
|
||||
/// An optional image loading service so that views can manage themselves
|
||||
/// Intentionally non-generic so that it doesn't grow uncontrollably
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
|
||||
|
||||
/// Set-able access to the bindable state.
|
||||
subscript<T>(dynamicMember keyPath: WritableKeyPath<State.BindStateType, T>) -> T {
|
||||
get { viewState.bindings[keyPath: keyPath] }
|
||||
set { viewState.bindings[keyPath: keyPath] = newValue }
|
||||
}
|
||||
|
||||
|
||||
/// Send a `ViewAction` to the `ViewModel` for processing.
|
||||
/// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`.
|
||||
func send(viewAction: ViewAction) {
|
||||
viewModel?.process(viewAction: viewAction)
|
||||
}
|
||||
|
||||
|
||||
fileprivate init(initialViewState: State, mediaProvider: MediaProviderProtocol?) {
|
||||
self.viewState = initialViewState
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
@@ -11,13 +11,13 @@ import SwiftUI
|
||||
struct AppActivityView: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = UIActivityViewController
|
||||
typealias CompletionType = (Result<(activity: UIActivity.ActivityType, items: [Any]?), Error>) -> Void
|
||||
|
||||
|
||||
private let activityItems: [Any]
|
||||
private let applicationActivities: [UIActivity]?
|
||||
private var excludedActivityTypes: [UIActivity.ActivityType]
|
||||
private var onCancel: (() -> Void)?
|
||||
private var onComplete: CompletionType?
|
||||
|
||||
|
||||
init(activityItems: [Any],
|
||||
applicationActivities: [UIActivity]? = nil,
|
||||
excludedActivityTypes: [UIActivity.ActivityType] = [],
|
||||
@@ -29,11 +29,11 @@ struct AppActivityView: UIViewControllerRepresentable {
|
||||
self.onCancel = onCancel
|
||||
self.onComplete = onComplete
|
||||
}
|
||||
|
||||
|
||||
func makeUIViewController(context: Context) -> UIViewControllerType {
|
||||
let viewController = UIViewControllerType(activityItems: activityItems, applicationActivities: applicationActivities)
|
||||
viewController.excludedActivityTypes = excludedActivityTypes
|
||||
|
||||
|
||||
viewController.completionWithItemsHandler = { activity, completed, items, error in
|
||||
if let error {
|
||||
onComplete?(.failure(error))
|
||||
@@ -45,12 +45,12 @@ struct AppActivityView: UIViewControllerRepresentable {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return viewController
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
|
||||
|
||||
|
||||
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Coordinator) {
|
||||
uiViewController.completionWithItemsHandler = nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
/// A button that contains text that is copied on tap
|
||||
struct CopyTextButton: View {
|
||||
let content: String
|
||||
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
UIPasteboard.general.string = content
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
struct JoinCallButton: View {
|
||||
let isVoiceCall: Bool
|
||||
let action: () -> Void
|
||||
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 26, *) {
|
||||
glassButton
|
||||
|
||||
@@ -358,7 +358,7 @@ struct LoadableImage_Previews: PreviewProvider, TestablePreview {
|
||||
mediaProvider: loadingMediaProvider,
|
||||
placeholder: placeholder)
|
||||
.layout(title: "Loading (avatar)")
|
||||
|
||||
|
||||
LoadableImage(url: "mxc://wherever/345",
|
||||
mediaType: .timelineItem(uniqueID: .init("id")),
|
||||
blurhash: "KbLM^j]q$jT|EfR-3rtjXk",
|
||||
@@ -388,7 +388,7 @@ struct LoadableImage_Previews: PreviewProvider, TestablePreview {
|
||||
static func placeholder() -> some View {
|
||||
Color.compound._bgBubbleIncoming
|
||||
}
|
||||
|
||||
|
||||
static func transformer(_ view: AnyView) -> some View {
|
||||
view.overlay {
|
||||
Image(systemSymbol: .playCircleFill)
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
|
||||
struct PlaceholderAvatarImage: View {
|
||||
@Environment(\.redactionReasons) private var redactionReasons
|
||||
|
||||
|
||||
private let textForImage: String
|
||||
private let contentID: String
|
||||
|
||||
@@ -32,18 +32,18 @@ struct PlaceholderAvatarImage: View {
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
}
|
||||
|
||||
|
||||
init(name: String?, contentID: String) {
|
||||
let baseName = name ?? contentID.trimmingCharacters(in: .punctuationCharacters)
|
||||
textForImage = baseName.first?.uppercased() ?? ""
|
||||
self.contentID = contentID
|
||||
}
|
||||
|
||||
|
||||
private var backgroundColor: Color {
|
||||
if redactionReasons.contains(.placeholder) {
|
||||
return Color(.systemGray4) // matches the default text redaction
|
||||
}
|
||||
|
||||
|
||||
return avatarColor?.background ?? .compound.iconPrimary
|
||||
}
|
||||
|
||||
|
||||
@@ -11,42 +11,42 @@ import SwiftUI
|
||||
struct RoundedCornerShape: Shape {
|
||||
let radius: CGFloat
|
||||
let corners: UIRectCorner
|
||||
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
|
||||
let width = rect.size.width
|
||||
let height = rect.size.height
|
||||
|
||||
|
||||
var topLeft: CGFloat = corners.contains(.topLeft) ? radius : 0.0
|
||||
var topRight: CGFloat = corners.contains(.topRight) ? radius : 0.0
|
||||
var bottomLeft: CGFloat = corners.contains(.bottomLeft) ? radius : 0.0
|
||||
var bottomRight: CGFloat = corners.contains(.bottomRight) ? radius : 0.0
|
||||
|
||||
|
||||
// Make sure we do not exceed the size of the rectangle
|
||||
topRight = min(min(topRight, height / 2), width / 2)
|
||||
topLeft = min(min(topLeft, height / 2), width / 2)
|
||||
bottomLeft = min(min(bottomLeft, height / 2), width / 2)
|
||||
bottomRight = min(min(bottomRight, height / 2), width / 2)
|
||||
|
||||
|
||||
path.move(to: CGPoint(x: width / 2.0, y: 0))
|
||||
path.addLine(to: CGPoint(x: width - topRight, y: 0))
|
||||
path.addArc(center: CGPoint(x: width - topRight, y: topRight), radius: topRight,
|
||||
startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
|
||||
|
||||
|
||||
path.addLine(to: CGPoint(x: width, y: height - bottomRight))
|
||||
path.addArc(center: CGPoint(x: width - bottomRight, y: height - bottomRight), radius: bottomRight,
|
||||
startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
|
||||
|
||||
|
||||
path.addLine(to: CGPoint(x: bottomLeft, y: height))
|
||||
path.addArc(center: CGPoint(x: bottomLeft, y: height - bottomLeft), radius: bottomLeft,
|
||||
startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
|
||||
|
||||
|
||||
path.addLine(to: CGPoint(x: 0, y: topLeft))
|
||||
path.addArc(center: CGPoint(x: topLeft, y: topLeft), radius: topLeft,
|
||||
startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
|
||||
path.closeSubpath()
|
||||
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ struct StackedAvatarsView_Previews: PreviewProvider, TestablePreview {
|
||||
.init(url: nil, name: "Charlie", contentID: "@charlie:matrix.org"),
|
||||
.init(url: nil, name: "Dan", contentID: "@charlie:matrix.org")
|
||||
]
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
VStack(spacing: 10) {
|
||||
StackedAvatarsView(overlap: 16,
|
||||
|
||||
@@ -16,29 +16,29 @@ struct TimelineScrollButton: View {
|
||||
case down
|
||||
case up
|
||||
}
|
||||
|
||||
|
||||
let direction: Direction
|
||||
let isHidden: Bool
|
||||
let showsBadge: Bool
|
||||
let onLongPress: (() -> Void)?
|
||||
let callback: () -> Void
|
||||
|
||||
|
||||
@ScaledMetric(relativeTo: .compound.bodyLG) private var iconSize: CGFloat = 24
|
||||
@ScaledMetric(relativeTo: .compound.bodyLG) private var badgeDotSize: CGFloat = 12
|
||||
@GestureState private var isPressing = false
|
||||
/// Set when a long-press is recognized so the trailing touch-up doesn't also fire `callback()`.
|
||||
@State private var consumedByLongPress = false
|
||||
|
||||
|
||||
/// Padding around the chevron, balanced so the total circle stays ~36pt at default text size.
|
||||
private let iconPadding: CGFloat = 6
|
||||
|
||||
|
||||
/// Vertical offset that half-centers the badge dot on the circle's edge. Magnitude is half
|
||||
/// the dot's diameter so half sits inside the circle, half outside. Sign is applied at the
|
||||
/// call site based on direction.
|
||||
private var badgeOffsetMagnitude: CGFloat {
|
||||
badgeDotSize / 2
|
||||
}
|
||||
|
||||
|
||||
init(direction: Direction = .down,
|
||||
isHidden: Bool = false,
|
||||
showsBadge: Bool = false,
|
||||
@@ -50,7 +50,7 @@ struct TimelineScrollButton: View {
|
||||
self.onLongPress = onLongPress
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
if let onLongPress {
|
||||
buttonContent
|
||||
@@ -76,7 +76,7 @@ struct TimelineScrollButton: View {
|
||||
buttonContent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var buttonContent: some View {
|
||||
Button {
|
||||
if consumedByLongPress {
|
||||
@@ -99,7 +99,7 @@ struct TimelineScrollButton: View {
|
||||
.accessibilityLabel(direction == .down ? L10n.a11yJumpToBottom : L10n.a11yJumpToUnread)
|
||||
.animation(.elementDefault, value: isHidden)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var icon: some View {
|
||||
let chevron = CompoundIcon(direction == .down ? \.chevronDown : \.chevronUp,
|
||||
@@ -117,7 +117,7 @@ struct TimelineScrollButton: View {
|
||||
.overlay(Circle().stroke(Color.compound.borderInteractiveSecondary, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var badge: some View {
|
||||
Circle()
|
||||
.fill(Color.compound.iconAccentPrimary)
|
||||
|
||||
@@ -15,7 +15,7 @@ struct ToolbarButton: View {
|
||||
static let done = Role.confirm(title: L10n.actionDone)
|
||||
static let save = Role.confirm(title: L10n.actionSave)
|
||||
static let close = Role.cancel(title: L10n.actionClose)
|
||||
|
||||
|
||||
case cancel(title: String)
|
||||
case confirm(title: String)
|
||||
case destructive(title: String)
|
||||
|
||||
@@ -46,7 +46,7 @@ extension View {
|
||||
func topBanner(_ banner: some View, isVisible: Bool, footer: some View = EmptyView()) -> some View {
|
||||
topBanners([TopBannerLayer(banner, isVisible: isVisible)], footer: footer)
|
||||
}
|
||||
|
||||
|
||||
/// Overlays the given Z-axis banner slots at the top edge of this view.
|
||||
/// Later items in the array are overlayed on top of earlier ones. Within
|
||||
/// each slot, visible vertical banners are stacked in a VStack and slide
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
/// Represents the position of a checkmark item in a list.
|
||||
enum ListPosition {
|
||||
case top, middle, bottom
|
||||
|
||||
|
||||
/// The corners that should be rounded for this position.
|
||||
var roundedCorners: UIRectCorner {
|
||||
switch self {
|
||||
@@ -57,11 +57,11 @@ private struct VisualListItemLabelStyle: LabelStyle {
|
||||
|
||||
struct VisualListItem_Previews: PreviewProvider, TestablePreview {
|
||||
static let strings = AnalyticsPromptScreenStrings(termsURL: AppSettings.volatile().analyticsTermsURL)
|
||||
|
||||
|
||||
static var testImage1: some View {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
|
||||
|
||||
static var testImage2: some View {
|
||||
Image(systemName: "square")
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ enum UserAgentBuilder {
|
||||
static func makeUserAgent() -> String? {
|
||||
let clientName = InfoPlistReader.app.bundleDisplayName
|
||||
let clientVersion = InfoPlistReader.app.bundleShortVersionString
|
||||
|
||||
|
||||
#if os(iOS)
|
||||
let scale = UIScreen.main.scale
|
||||
return if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
|
||||
@@ -17,7 +17,7 @@ enum UserIndicatorType: Equatable {
|
||||
static var toast: Self {
|
||||
.toast(progress: .none)
|
||||
}
|
||||
|
||||
|
||||
static var modal: Self {
|
||||
.modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: false)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
|
||||
guard delayedIndicators.contains(indicator.id) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
enqueue(indicator: indicator)
|
||||
}
|
||||
} else {
|
||||
@@ -85,7 +85,7 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private func enqueue(indicator: UserIndicator) {
|
||||
retractIndicatorWithId(indicator.id)
|
||||
indicatorQueue.append(indicator)
|
||||
|
||||
@@ -13,7 +13,7 @@ import SwiftUI
|
||||
struct UserIndicatorModalView: View {
|
||||
let indicator: UserIndicator
|
||||
@State private var progressFraction = 0.0
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(spacing: 12.0) {
|
||||
@@ -22,7 +22,7 @@ struct UserIndicatorModalView: View {
|
||||
} else if case .published = indicator.progress {
|
||||
ProgressView(value: progressFraction)
|
||||
}
|
||||
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if let icon = indicator.icon {
|
||||
CompoundIcon(icon, size: iconSize, relativeTo: titleFont)
|
||||
|
||||
@@ -20,7 +20,7 @@ extension EstimatedWaveform {
|
||||
guard maxSamplesCount > 0 else {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
// Filter the data to keep only the expected number of samples
|
||||
let result: [UInt16]
|
||||
if data.count > maxSamplesCount {
|
||||
@@ -32,7 +32,7 @@ extension EstimatedWaveform {
|
||||
} else {
|
||||
result = data
|
||||
}
|
||||
|
||||
|
||||
// Normalize the sample in the allowed range
|
||||
return result.map { Float($0) / Float(Self.dataRange.upperBound) }
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import SwiftUI
|
||||
struct PlaybackSpeedButton: View {
|
||||
let speed: AudioPlaybackSpeed
|
||||
let onTap: () -> Void
|
||||
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
ZStack {
|
||||
Text(speed.placeholder)
|
||||
.font(.compound.bodyXSSemibold)
|
||||
.hidden()
|
||||
|
||||
|
||||
Text(speed.label)
|
||||
.font(.compound.bodyXSSemibold)
|
||||
.foregroundColor(.compound.iconSecondary)
|
||||
|
||||
@@ -44,7 +44,7 @@ struct VoiceMessageButton: View {
|
||||
self.state = state
|
||||
self.action = action
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
buttonLabel
|
||||
@@ -58,7 +58,7 @@ struct VoiceMessageButton: View {
|
||||
.disabled(state == .loading)
|
||||
.accessibilityLabel(accessibilityLabel)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var buttonLabel: some View {
|
||||
switch state {
|
||||
@@ -87,7 +87,7 @@ private struct VoiceMessageButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) var isEnabled: Bool
|
||||
|
||||
let color: Color
|
||||
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.foregroundColor(isEnabled ? color.opacity(configuration.isPressed ? 0.6 : 1) : .compound.iconDisabled)
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
|
||||
struct WaveformCursorView: View {
|
||||
var color: Color = .compound.iconAccentTertiary
|
||||
|
||||
|
||||
var body: some View {
|
||||
RoundedRectangle(cornerRadius: 1)
|
||||
.fill(color)
|
||||
|
||||
@@ -19,11 +19,11 @@ private struct WaveformInteractionModifier: ViewModifier {
|
||||
let progress: Double
|
||||
let showCursor: Bool
|
||||
let onSeek: (Double) -> Void
|
||||
|
||||
|
||||
@ScaledMetric private var cursorVisibleWidth = 2.0
|
||||
@ScaledMetric private var cursorVisibleHeight = 24.0
|
||||
private let cursorInteractiveSize: CGFloat = 50
|
||||
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
GeometryReader { geometry in
|
||||
content
|
||||
@@ -52,6 +52,6 @@ private struct WaveformInteractionModifier: ViewModifier {
|
||||
.coordinateSpace(name: Self.namespaceName)
|
||||
.animation(nil, value: progress)
|
||||
}
|
||||
|
||||
|
||||
private static let namespaceName = "voice-message-waveform"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user