Some random tweaks made on a train 🚆 (#4636)

* Fix the search text field's tint colour.

* Don't allow optional content IDs in the placeholder avatar.

* Use SwiftUI to resolve the hex values in the Inspector app.

This fixes incorrect values being shown in dark/high-contrast modes.

* Fix a layout bug with the colour swatch in the Inspector app on iPhone.

* Switch to the chats tab when selecting a room with the global search screen.

* Run the latest SwiftFormat.
This commit is contained in:
Doug
2025-10-21 11:04:54 +02:00
committed by GitHub
parent 325fe09c98
commit bab9b89416
12 changed files with 55 additions and 68 deletions

View File

@@ -11,6 +11,7 @@ import MatrixRustSDK
import SwiftUI import SwiftUI
enum ChatsFlowCoordinatorAction { enum ChatsFlowCoordinatorAction {
case switchToChatsTab
case showSettings case showSettings
case showChatBackupSettings case showChatBackupSettings
case sessionVerification(SessionVerificationScreenFlow) case sessionVerification(SessionVerificationScreenFlow)
@@ -661,6 +662,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol {
case .select(let roomID): case .select(let roomID):
dismissGlobalSearch() dismissGlobalSearch()
handleAppRoute(.room(roomID: roomID, via: []), animated: true) handleAppRoute(.room(roomID: roomID, via: []), animated: true)
actionsSubject.send(.switchToChatsTab)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)

View File

@@ -193,6 +193,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
.sink { [weak self] action in .sink { [weak self] action in
guard let self else { return } guard let self else { return }
switch action { switch action {
case .switchToChatsTab:
navigationTabCoordinator.selectedTab = .chats
case .showSettings: case .showSettings:
handleAppRoute(.settings, animated: true) handleAppRoute(.settings, animated: true)
case .showChatBackupSettings: case .showChatBackupSettings:

View File

@@ -10,7 +10,7 @@ import SwiftUI
struct LoadableAvatarImage: View { struct LoadableAvatarImage: View {
private let url: URL? private let url: URL?
private let name: String? private let name: String?
private let contentID: String? private let contentID: String
private let isSpace: Bool private let isSpace: Bool
private let avatarSize: Avatars.Size private let avatarSize: Avatars.Size
private let mediaProvider: MediaProviderProtocol? private let mediaProvider: MediaProviderProtocol?
@@ -20,7 +20,7 @@ struct LoadableAvatarImage: View {
init(url: URL?, init(url: URL?,
name: String?, name: String?,
contentID: String?, contentID: String,
isSpace: Bool = false, isSpace: Bool = false,
avatarSize: Avatars.Size, avatarSize: Avatars.Size,
mediaProvider: MediaProviderProtocol?, mediaProvider: MediaProviderProtocol?,

View File

@@ -5,28 +5,16 @@
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import Compound
import SwiftUI import SwiftUI
struct OverridableAvatarImage: View { struct OverridableAvatarImage: View {
private let overrideURL: URL? let overrideURL: URL?
private let url: URL? let url: URL?
private let name: String? let name: String?
private let contentID: String? let contentID: String
private let avatarSize: Avatars.Size let avatarSize: Avatars.Size
private let mediaProvider: MediaProviderProtocol? let mediaProvider: MediaProviderProtocol?
@ScaledMetric private var frameSize: CGFloat
init(overrideURL: URL?, url: URL?, name: String?, contentID: String?, avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol?) {
self.overrideURL = overrideURL
self.url = url
self.name = name
self.contentID = contentID
self.avatarSize = avatarSize
self.mediaProvider = mediaProvider
_frameSize = ScaledMetric(wrappedValue: avatarSize.value)
}
var body: some View { var body: some View {
if let overrideURL { if let overrideURL {
@@ -37,7 +25,7 @@ struct OverridableAvatarImage: View {
} placeholder: { } placeholder: {
ProgressView() ProgressView()
} }
.frame(width: frameSize, height: frameSize) .scaledFrame(size: avatarSize.value)
.clipShape(Circle()) .clipShape(Circle())
} else { } else {
LoadableAvatarImage(url: url, LoadableAvatarImage(url: url,

View File

@@ -12,7 +12,7 @@ struct PlaceholderAvatarImage: View {
@Environment(\.redactionReasons) private var redactionReasons @Environment(\.redactionReasons) private var redactionReasons
private let textForImage: String private let textForImage: String
private let contentID: String? private let contentID: String
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
@@ -32,9 +32,9 @@ struct PlaceholderAvatarImage: View {
.aspectRatio(1, contentMode: .fill) .aspectRatio(1, contentMode: .fill)
} }
init(name: String?, contentID: String?) { init(name: String?, contentID: String) {
let baseName = name ?? contentID?.trimmingCharacters(in: .punctuationCharacters) let baseName = name ?? contentID.trimmingCharacters(in: .punctuationCharacters)
textForImage = baseName?.first?.uppercased() ?? "" textForImage = baseName.first?.uppercased() ?? ""
self.contentID = contentID self.contentID = contentID
} }
@@ -47,11 +47,7 @@ struct PlaceholderAvatarImage: View {
} }
private var avatarColor: DecorativeColor? { private var avatarColor: DecorativeColor? {
guard let contentID else { Color.compound.decorativeColor(for: contentID)
return nil
}
return Color.compound.decorativeColor(for: contentID)
} }
} }

View File

@@ -69,7 +69,7 @@ struct RoomAvatarImage: View {
// We will expand upon this with more stack sizes in the future. // We will expand upon this with more stack sizes in the future.
if users.count == 0 { if users.count == 0 {
let _ = assertionFailure("We should never pass empty heroes here.") let _ = assertionFailure("We should never pass empty heroes here.")
PlaceholderAvatarImage(name: nil, contentID: nil) PlaceholderAvatarImage(name: nil, contentID: "")
} else if users.count == 2 { } else if users.count == 2 {
let clusterSize = avatarSize.value * 1.6 let clusterSize = avatarSize.value * 1.6
ZStack { ZStack {

View File

@@ -133,7 +133,7 @@ struct TimelineThreadSummaryView: View {
LoadableAvatarImage(url: sender?.avatarURL, LoadableAvatarImage(url: sender?.avatarURL,
name: sender?.displayName, name: sender?.displayName,
contentID: sender?.id, contentID: senderID,
avatarSize: .user(on: .threadSummary), avatarSize: .user(on: .threadSummary),
mediaProvider: context.mediaProvider) mediaProvider: context.mediaProvider)
.accessibilityHidden(true) .accessibilityHidden(true)

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:560872aaa26dfa28023e7f73145ad82959dcd69a0afdc1174465108182d5e3b8 oid sha256:f52d1b62d203f60d05624be87c8044946dbfb4e0d2065b486c48ab34116d1ea0
size 180990 size 183671

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c7f55a94cf459288c4a34c8b553038be8eb41acf7ad67268eea3ca68a4a5a9a5 oid sha256:8ff95fcc75e71bd2a6b1225a6ad56eaff8fa45a00aae77ea738e793976100f69
size 143969 size 145788

View File

@@ -47,7 +47,7 @@ struct BuildSDK: AsyncParsableCommand {
Run the following command to install them: Run the following command to install them:
rustup target add \(missingTargets.joined(separator: " ")) rustup target add \(missingTargets.joined(separator: " "))
""" """
default: default:
return nil return nil

View File

@@ -19,6 +19,8 @@ struct ColorsScreen: View {
} }
struct ColorItem: View { struct ColorItem: View {
@Environment(\.self) private var environment
let color: Color let color: Color
let name: String let name: String
@@ -30,10 +32,11 @@ struct ColorItem: View {
Text(name) Text(name)
.font(.compound.bodyLG) .font(.compound.bodyLG)
.foregroundColor(.compound.textPrimary) .foregroundColor(.compound.textPrimary)
Text(color.hexValue()) Text(color.hexValue(in: environment))
.font(.compound.bodySM.monospaced()) .font(.compound.bodySM.monospaced())
.foregroundColor(.compound.textSecondary) .foregroundColor(.compound.textSecondary)
} }
.layoutPriority(1)
} }
} }
@@ -55,24 +58,24 @@ struct ColorItem: View {
} }
private extension Color { private extension Color {
func hexValue() -> String { func hexValue(in environment: EnvironmentValues) -> String {
let uiColor = UIColor(self) let resolved = resolve(in: environment)
return if resolved.opacity == 1 {
var red: CGFloat = 0 "#\(resolved.red.asHex)\(resolved.green.asHex)\(resolved.blue.asHex)"
var green: CGFloat = 0 } else {
var blue: CGFloat = 0 "#\(resolved.red.asHex)\(resolved.green.asHex)\(resolved.blue.asHex) (\(resolved.opacity.asPercentage) opacity)"
var alpha: CGFloat = 0 }
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
return "#\(red.asHex)\(green.asHex)\(blue.asHex)"
} }
} }
private extension CGFloat { private extension Float {
var asHex: String { var asHex: String {
String(format: "%02X", Int((self * 255).rounded())) String(format: "%02X", Int((self * 255).rounded()))
} }
var asPercentage: String {
String(format: "%.0f%%", self * 100)
}
} }
struct ColorsScreen_Previews: PreviewProvider { struct ColorsScreen_Previews: PreviewProvider {

View File

@@ -14,21 +14,18 @@ public extension View {
@MainActor @MainActor
@ViewBuilder @ViewBuilder
func compoundSearchField() -> some View { func compoundSearchField() -> some View {
if #available(iOS 26, *) { introspect(.navigationStack, on: .supportedVersions, scope: .ancestor) { navigationController in
self // Uses the navigation stack as .searchField is unreliable when pushing the second search bar, during the create rooms flow.
} else { guard let searchController = navigationController.navigationBar.topItem?.searchController else { return }
introspect(.navigationStack, on: .supportedVersions, scope: .ancestor) { navigationController in
// Uses the navigation stack as .searchField is unreliable when pushing the second search bar, during the create rooms flow. // Ported from Riot iOS as this is the only reliable way to get the exact look we want.
guard let searchController = navigationController.navigationBar.topItem?.searchController else { return } // However this is fragile and tied to gutwrenching the current UISearchBar internals.
// Ported from Riot iOS as this is the only reliable way to get the exact look we want. let searchTextField = searchController.searchBar.searchTextField
// However this is fragile and tied to gutwrenching the current UISearchBar internals. searchTextField.tintColor = .compound.iconAccentTertiary
let textColor = UIColor.compound.textPrimary
if #unavailable(iOS 26.0) {
let placeholderColor = UIColor.compound.textSecondary let placeholderColor = UIColor.compound.textSecondary
let textFieldTintColor = UIColor.compound.iconAccentTertiary
let textFieldBackgroundColor = UIColor.compound._bgSubtleSecondaryAlpha
let searchTextField = searchController.searchBar.searchTextField
// Magnifying glass icon. // Magnifying glass icon.
let leftImageView = searchTextField.leftView as? UIImageView let leftImageView = searchTextField.leftView as? UIImageView
@@ -43,9 +40,8 @@ public extension View {
clearButton?.tintColor = placeholderColor clearButton?.tintColor = placeholderColor
// Text field. // Text field.
searchTextField.textColor = textColor searchTextField.textColor = .compound.textPrimary
searchTextField.backgroundColor = textFieldBackgroundColor searchTextField.backgroundColor = .compound._bgSubtleSecondaryAlpha
searchTextField.tintColor = textFieldTintColor
// Hide the effect views so we can use the rounded rect style without any materials. // Hide the effect views so we can use the rounded rect style without any materials.
let effectBackgroundTop = searchTextField.value(forKey: "_effectBackgroundTop") as? UIView let effectBackgroundTop = searchTextField.value(forKey: "_effectBackgroundTop") as? UIView