aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj12
-rw-r--r--Hotline/Application-macOS.swift11
-rw-r--r--Hotline/Models/Hotline.swift7
-rw-r--r--Hotline/Utility/FoundationExtensions.swift12
-rw-r--r--Hotline/Utility/NSWindowBridge.swift25
-rw-r--r--Hotline/Utility/RegularExpressions.swift96
-rw-r--r--Hotline/macOS/AboutView.swift52
-rw-r--r--Hotline/macOS/ChatView.swift34
-rw-r--r--Hotline/macOS/ServerAgreementView.swift81
-rw-r--r--Hotline/macOS/ServerMessageView.swift26
-rw-r--r--Hotline/macOS/ServerView.swift8
-rw-r--r--Hotline/macOS/SettingsView.swift1
12 files changed, 310 insertions, 55 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index 1f60d6d..2488d13 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -30,6 +30,9 @@
DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */; };
DA57536C2B36BA1D00FAC277 /* TextDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57536B2B36BA1D00FAC277 /* TextDocument.swift */; };
DA6300972B24036B0034CBFD /* HotlineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6300962B24036B0034CBFD /* HotlineClient.swift */; };
+ DA65499A2BEC280E00EDB697 /* ServerMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6549992BEC280E00EDB697 /* ServerMessageView.swift */; };
+ DA65499C2BEC3FBD00EDB697 /* ServerAgreementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA65499B2BEC3FBD00EDB697 /* ServerAgreementView.swift */; };
+ DA65499E2BEC438A00EDB697 /* NSWindowBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA65499D2BEC438A00EDB697 /* NSWindowBridge.swift */; };
DA72A0DD2B4CD0BF00A0F48A /* NewsEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA72A0DC2B4CD0BF00A0F48A /* NewsEditorView.swift */; };
DA72A0E02B4DA8CA00A0F48A /* SplitView in Frameworks */ = {isa = PBXBuildFile; productRef = DA72A0DF2B4DA8CA00A0F48A /* SplitView */; };
DA72A0E22B4DAA4000A0F48A /* NewsArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */; };
@@ -105,6 +108,9 @@
DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileClient.swift; sourceTree = "<group>"; };
DA57536B2B36BA1D00FAC277 /* TextDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDocument.swift; sourceTree = "<group>"; };
DA6300962B24036B0034CBFD /* HotlineClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineClient.swift; sourceTree = "<group>"; };
+ DA6549992BEC280E00EDB697 /* ServerMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMessageView.swift; sourceTree = "<group>"; };
+ DA65499B2BEC3FBD00EDB697 /* ServerAgreementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerAgreementView.swift; sourceTree = "<group>"; };
+ DA65499D2BEC438A00EDB697 /* NSWindowBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWindowBridge.swift; sourceTree = "<group>"; };
DA72A0DC2B4CD0BF00A0F48A /* NewsEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsEditorView.swift; sourceTree = "<group>"; };
DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsArticle.swift; sourceTree = "<group>"; };
DA77253E2B21176D006C5ABB /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = "<group>"; };
@@ -275,6 +281,7 @@
DA57536B2B36BA1D00FAC277 /* TextDocument.swift */,
DABE8C032B57940A00884D28 /* DAKeychain.swift */,
DA55AC782BE6A1AD00034857 /* RegularExpressions.swift */,
+ DA65499D2BEC438A00EDB697 /* NSWindowBridge.swift */,
);
path = Utility;
sourceTree = "<group>";
@@ -319,6 +326,8 @@
DA2863D72B37AD1C00A7D050 /* SettingsView.swift */,
DAE136B92B9D1147007D8307 /* HotlinePanelView.swift */,
DA55AC762BE589F700034857 /* AboutView.swift */,
+ DA65499B2BEC3FBD00EDB697 /* ServerAgreementView.swift */,
+ DA6549992BEC280E00EDB697 /* ServerMessageView.swift */,
);
path = macOS;
sourceTree = "<group>";
@@ -414,6 +423,7 @@
DA43205E2B1D615600FC8843 /* ServerView.swift in Sources */,
DA0D69912B1E894800C71DF5 /* FilesView.swift in Sources */,
DAC3D9832BC33FD000A727C9 /* ApplicationState.swift in Sources */,
+ DA65499C2BEC3FBD00EDB697 /* ServerAgreementView.swift in Sources */,
DAB4D8802B4C8E9A0048A05C /* URLAdditions.swift in Sources */,
DA2863DD2B3E8B7000A7D050 /* FilePreview.swift in Sources */,
DA2863D82B37AD1C00A7D050 /* SettingsView.swift in Sources */,
@@ -433,6 +443,7 @@
DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */,
DAB4D87B2B4B78310048A05C /* FileImageView.swift in Sources */,
DA55AC732BE42AF000034857 /* AsyncLinkPreview.swift in Sources */,
+ DA65499E2BEC438A00EDB697 /* NSWindowBridge.swift in Sources */,
DAE735032B30C0BB000C56F6 /* MessageView.swift in Sources */,
DABE8C062B57A06100884D28 /* Bookmarks.swift in Sources */,
DA32CD4B2B29318E0053B98B /* FileInfo.swift in Sources */,
@@ -455,6 +466,7 @@
DA4F2BF82B16A17200D8ADDC /* HotlineProtocol.swift in Sources */,
DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */,
DAB4D8822B4C8FED0048A05C /* FileIconView.swift in Sources */,
+ DA65499A2BEC280E00EDB697 /* ServerMessageView.swift in Sources */,
DAAEE66F2B47625600A5BA07 /* FilePreviewImageView.swift in Sources */,
DA872B152BDDEE1A008B1012 /* VisualEffectView.swift in Sources */,
DAAEE66D2B475F1400A5BA07 /* PreviewFileInfo.swift in Sources */,
diff --git a/Hotline/Application-macOS.swift b/Hotline/Application-macOS.swift
index ccc4b15..09d6d91 100644
--- a/Hotline/Application-macOS.swift
+++ b/Hotline/Application-macOS.swift
@@ -49,9 +49,9 @@ struct Application: App {
.frame(minWidth: 250, minHeight: 250)
.environment(bookmarks)
}
- .keyboardShortcut(.init(.init("R"), modifiers: .command))
.defaultSize(width: 700, height: 550)
.defaultPosition(.center)
+ .keyboardShortcut(.init("R"), modifiers: .command)
.onChange(of: AppLaunchState.shared.launchState) {
if AppLaunchState.shared.launchState == .launched {
if Prefs.shared.showBannerToolbar {
@@ -60,21 +60,20 @@ struct Application: App {
}
}
+ // MARK: About Box
Window("About", id: "about") {
AboutView()
.ignoresSafeArea()
.background(Color.hotlineRed)
}
.windowResizability(.contentSize)
-// .windowStyle(.hiddenTitleBar)
- .windowStyle(HiddenTitleBarWindowStyle())
+ .windowStyle(.hiddenTitleBar)
.defaultPosition(.center)
+ .commandsRemoved() // Remove About that was automatically added to Window menu.
.commands {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
- Button(action: {
+ Button("About Hotline") {
openWindow(id: "about")
- }) {
- Text("About Hotline")
}
}
}
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index d232533..598c787 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -723,7 +723,12 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
print("Hotline: received private message from \(user.name): \(message)")
if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound {
- SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
+ if self.unreadInstantMessages[userID] == nil {
+ SoundEffectPlayer.shared.playSoundEffect(.serverMessage)
+ }
+ else {
+ SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
+ }
}
let instantMessage = InstantMessage(direction: .incoming, text: message.convertingLinksToMarkdown(), type: .message, date: Date())
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index 6270ac7..3a325be 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -11,12 +11,22 @@ extension String {
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
let matches = self.ranges(of: RegularExpressions.relaxedLink)
for match in matches {
- attributedString.addAttribute(.link, value: self[match], range: NSRange(match, in: self))
+ let matchString = String(self[match])
+ if matchString.isEmailAddress() {
+ attributedString.addAttribute(.link, value: "mailto:\(matchString)", range: NSRange(match, in: self))
+ }
+ else {
+ attributedString.addAttribute(.link, value: matchString, range: NSRange(match, in: self))
+ }
// attributedString.addAttribute(.underlineStyle, value: 1, range: NSRange(match, in: self))
}
return AttributedString(attributedString)
}
+ func isEmailAddress() -> Bool {
+ self.wholeMatch(of: RegularExpressions.emailAddress) != nil
+ }
+
func isWebURL() -> Bool {
guard let url = URL(string: self) else {
return false
diff --git a/Hotline/Utility/NSWindowBridge.swift b/Hotline/Utility/NSWindowBridge.swift
new file mode 100644
index 0000000..5cd7197
--- /dev/null
+++ b/Hotline/Utility/NSWindowBridge.swift
@@ -0,0 +1,25 @@
+import SwiftUI
+
+fileprivate class NSWindowAccessorView: NSView {
+ let executeBlock: (_ window: NSWindow? ) -> ()
+
+ init(_ inConfigFunction: @escaping (_ window: NSWindow? ) -> () ) {
+ executeBlock = inConfigFunction
+ super.init( frame: NSRect() )
+ }
+
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+
+ public override func viewDidMoveToWindow() {
+ super.viewDidMoveToWindow()
+ executeBlock( self.window ) // We pass it through even if it is nil.
+ }
+}
+
+public struct NSWindowAccessor: NSViewRepresentable {
+ var configCode: (_ window: NSWindow? ) -> ()
+
+ public init(_ configCode: @escaping (_: NSWindow?) -> Void) { self.configCode = configCode }
+ public func makeNSView(context: Context) -> NSView { return NSWindowAccessorView( configCode ) }
+ public func updateNSView(_ nsView: NSView, context: Context) {}
+}
diff --git a/Hotline/Utility/RegularExpressions.swift b/Hotline/Utility/RegularExpressions.swift
index bb53f8c..c1f2a18 100644
--- a/Hotline/Utility/RegularExpressions.swift
+++ b/Hotline/Utility/RegularExpressions.swift
@@ -48,7 +48,7 @@ struct RegularExpressions {
// domain name
OneOrMore {
CharacterClass(
- .anyOf(".-"),
+ .anyOf(".-@"),
("a"..."z"),
("0"..."9")
)
@@ -138,4 +138,98 @@ struct RegularExpressions {
}
.anchorsMatchLineEndings()
.ignoresCase()
+
+ static let emailAddress = Regex {
+ ChoiceOf {
+ Anchor.startOfLine
+ Anchor.wordBoundary
+ }
+ Capture {
+ // username
+ OneOrMore {
+ CharacterClass(
+ .anyOf(".-_"),
+ ("a"..."z"),
+ ("0"..."9")
+ )
+ }
+ "@"
+ // domain name
+ OneOrMore {
+ CharacterClass(
+ .anyOf(".-"),
+ ("a"..."z"),
+ ("0"..."9")
+ )
+ }
+ // top-level domain name
+ "."
+ ChoiceOf {
+ "com"
+ "net"
+ "org"
+ "edu"
+ "gov"
+ "mil"
+ "aero"
+ "asia"
+ "biz"
+ "cat"
+ "coop"
+ "info"
+ "int"
+ "jobs"
+ "mobi"
+ "museum"
+ "name"
+ "pizza"
+ "post"
+ "pro"
+ "red"
+ "tel"
+ "today"
+ "travel"
+ "garden"
+ "online"
+ "ai"
+ "be"
+ "by"
+ "ca"
+ "co"
+ "de"
+ "er"
+ "es"
+ "fr"
+ "gs"
+ "ie"
+ "im"
+ "in"
+ "io"
+ "is"
+ "it"
+ "jp"
+ "la"
+ "ly"
+ "ma"
+ "md"
+ "me"
+ "my"
+ "nl"
+ "ps"
+ "pt"
+ "ja"
+ "st"
+ "to"
+ "tv"
+ "uk"
+ "ws"
+ }
+ }
+ ChoiceOf {
+ Anchor.endOfLine
+ Anchor.wordBoundary
+ }
+ }
+ .anchorsMatchLineEndings()
+ .ignoresCase()
}
diff --git a/Hotline/macOS/AboutView.swift b/Hotline/macOS/AboutView.swift
index 7bd1c54..1cf7dc2 100644
--- a/Hotline/macOS/AboutView.swift
+++ b/Hotline/macOS/AboutView.swift
@@ -77,11 +77,12 @@ struct AboutView: View {
}
}
.frame(height: 40)
-// .padding(.top, 4)
Spacer()
}
- .frame(width: 270)
+ .frame(width: 250)
+
+ Spacer()
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
@@ -118,17 +119,38 @@ struct AboutView: View {
Link(destination: contributor.webURL) {
HStack {
if let pictureURL = contributor.pictureURL {
- AsyncImage(url: pictureURL) { img in
- img
- .interpolation(.high)
- .resizable()
- .scaledToFit()
- .background(.white)
- } placeholder: {
- Color.white.opacity(0.2)
+ AsyncImage(url: pictureURL) { phase in
+ if let image = phase.image {
+ image
+ .interpolation(.high)
+ .resizable()
+ .scaledToFit()
+ .background(.white)
+ .frame(width: 32, height: 32)
+ } else if phase.error != nil {
+ Color.clear
+ .frame(width: 32, height: 32)
+ } else {
+ Color.white
+ .opacity(0.2)
+ .frame(width: 32, height: 32)
+ }
}
.frame(width: 32, height: 32)
.clipShape(Circle())
+
+// AsyncImage(url: pictureURL) { img in
+// img
+// .interpolation(.high)
+// .resizable()
+// .scaledToFit()
+// .background(.white)
+// } placeholder: {
+// Color.white.opacity(0.2)
+// .frame(width: 32, height: 32)
+// }
+// .frame(width: 32, height: 32)
+// .clipShape(Circle())
}
VStack(alignment: .leading, spacing: 2) {
@@ -143,16 +165,12 @@ struct AboutView: View {
.font(.system(size: 11))
.foregroundStyle(.white.opacity(0.4))
}
-
- Spacer()
}
}
}
}
}
- .ignoresSafeArea()
.scrollClipDisabled()
- .padding(.leading, 24)
}
.frame(width: 570, height: 330)
.background(
@@ -162,7 +180,7 @@ struct AboutView: View {
Spacer()
}
.frame(height: 330 + 100)
- .offset(x: 270)
+ .offset(x: 250)
}
)
.background(Color.hotlineRed)
@@ -191,7 +209,9 @@ struct AboutView: View {
}
}
- contributors = newContributors
+ withAnimation {
+ contributors = newContributors
+ }
}
func checkForUpdate() async {
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift
index 98fb852..0875106 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/ChatView.swift
@@ -44,35 +44,17 @@ struct ChatView: View {
.clipShape(RoundedRectangle(cornerRadius: 3))
}
#endif
- ScrollView(.vertical) {
- HStack {
- Spacer()
- Text(msg.text.convertToAttributedStringWithLinks())
- .font(.system(size: 12))
- .fontDesign(.monospaced)
- .textSelection(.enabled)
- .tint(Color("Link Color"))
- .frame(maxWidth: 400, alignment: .center)
- .padding(16)
- Spacer()
- }
- }
- .frame(maxWidth: .infinity, maxHeight: 375)
- .scrollBounceBehavior(.basedOnSize)
-#if os(iOS)
- .background(Color("Agreement Background"))
-#elseif os(macOS)
- .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow))
-#endif
- .clipShape(RoundedRectangle(cornerRadius: 8))
- .padding(.bottom, 16)
+ ServerAgreementView(text: msg.text)
+ .padding(.bottom, 16)
}
// MARK: Server Message
else if msg.type == .server {
- Text(msg.text)
- .lineSpacing(4)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
+ HStack {
+ Spacer()
+ ServerMessageView(message: msg.text)
+ Spacer()
+ }
+ .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
}
// MARK: Status
else if msg.type == .status {
diff --git a/Hotline/macOS/ServerAgreementView.swift b/Hotline/macOS/ServerAgreementView.swift
new file mode 100644
index 0000000..a1f96d9
--- /dev/null
+++ b/Hotline/macOS/ServerAgreementView.swift
@@ -0,0 +1,81 @@
+import SwiftUI
+
+fileprivate let MAX_AGREEMENT_HEIGHT: CGFloat = 280
+
+struct ServerAgreementView: View {
+ let text: String
+
+ @State private var expandable: Bool = false
+ @State private var expanded: Bool = false
+
+ var body: some View {
+ ScrollView(.vertical) {
+ HStack(alignment: .top) {
+ Spacer()
+ Text(text.convertToAttributedStringWithLinks())
+ .font(.system(size: 12))
+ .fontDesign(.monospaced)
+ .textSelection(.enabled)
+ .tint(Color("Link Color"))
+ .frame(maxWidth: 400)
+ .padding(16)
+ .background(
+ GeometryReader { geometry in
+ Color.clear.onAppear {
+ if geometry.size.height > MAX_AGREEMENT_HEIGHT {
+ expandable = true
+ }
+ else {
+ expandable = false
+ }
+ }
+ }
+ )
+ Spacer()
+ }
+ }
+ .scrollIndicators(.never)
+ .frame(maxWidth: .infinity, maxHeight: (expandable && expanded) ? nil : MAX_AGREEMENT_HEIGHT)
+ .scrollBounceBehavior(.basedOnSize)
+#if os(iOS)
+ .background(Color("Agreement Background"))
+#elseif os(macOS)
+ .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow))
+#endif
+ .overlay(
+ ZStack(alignment: .bottomTrailing) {
+ Group {
+ if !expandable || expanded {
+ EmptyView()
+ }
+ else {
+ Button(action: {
+ withAnimation(.easeOut(duration: 0.15)) {
+ expanded = true
+ }
+ }, label: {
+ Color.black
+ .opacity(0.00001)
+ .frame(width: 32, height: 32)
+ .overlay(
+ Image(systemName: "arrow.up.left.and.arrow.down.right")
+ .resizable()
+ .scaledToFit()
+ .fontWeight(.semibold)
+ .frame(width: 12, height: 12)
+ .foregroundColor(.primary.opacity(0.8))
+ , alignment: .center)
+ })
+ .buttonStyle(.plain)
+ .help("Expand Server Agreement")
+ }
+ }
+ }
+ , alignment: .bottomTrailing)
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
+}
+
+#Preview {
+ ServerAgreementView(text: "Hello there and welcome to this server.")
+}
diff --git a/Hotline/macOS/ServerMessageView.swift b/Hotline/macOS/ServerMessageView.swift
new file mode 100644
index 0000000..373c2a6
--- /dev/null
+++ b/Hotline/macOS/ServerMessageView.swift
@@ -0,0 +1,26 @@
+import SwiftUI
+
+struct ServerMessageView: View {
+ let message: String
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 8) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .symbolRenderingMode(.multicolor)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16)
+ Text("**\(message)**")
+ .lineSpacing(4)
+ .multilineTextAlignment(.leading)
+ .textSelection(.enabled)
+ }
+ .padding()
+ .background(Color("Agreement Background"))
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
+}
+
+#Preview {
+ ServerMessageView(message: "This server has something important to say.")
+}
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index 190bcc2..21d7cbb 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -400,19 +400,19 @@ struct ServerView: View {
}
Text(user.name)
- .foregroundStyle(user.isAdmin ? Color(hex: 0xE10000) : .primary)
+ .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary)
Spacer()
if model.hasUnreadInstantMessages(userID: user.id) {
Circle()
.frame(width: 6, height: 6)
+ .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary.opacity(0.5))
.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 2))
- .opacity(0.5)
}
}
- .opacity(user.isIdle ? 0.6 : 1.0)
- .opacity(controlActiveState == .inactive ? 0.4 : 1.0)
+ .opacity(user.isIdle ? 0.5 : 1.0)
+ .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
.tag(ServerNavigationType.user(userID: user.id))
}
}
diff --git a/Hotline/macOS/SettingsView.swift b/Hotline/macOS/SettingsView.swift
index 24cfddf..79f0156 100644
--- a/Hotline/macOS/SettingsView.swift
+++ b/Hotline/macOS/SettingsView.swift
@@ -71,6 +71,7 @@ struct IconSettingsView: View {
.interpolation(.none)
.scaledToFit()
.frame(width: 32, height: 16)
+ .help("Icon \(String(iconID))")
}
.tag(iconID)
.frame(width: 32, height: 32)