Add instance announcements

Closes #356
This commit is contained in:
Shadowfacts 2024-04-18 00:00:00 -04:00
parent 4ecc16a93b
commit b89df3f27b
13 changed files with 930 additions and 1 deletions

View File

@ -209,6 +209,10 @@ public final class InstanceFeatures: ObservableObject {
}
}
public var instanceAnnouncements: Bool {
hasMastodonVersion(3, 1, 0)
}
public init() {
}

View File

@ -0,0 +1,99 @@
//
// Announcement.swift
// Pachyderm
//
// Created by Shadowfacts on 4/16/24.
//
import Foundation
import WebURL
public struct Announcement: Decodable, Sendable, Hashable, Identifiable {
public let id: String
public let content: String
public let startsAt: Date?
public let endsAt: Date?
public let allDay: Bool
public let publishedAt: Date
public let updatedAt: Date
public let read: Bool?
public let mentions: [Account]
public let statuses: [Status]
public let tags: [Hashtag]
public let emojis: [Emoji]
public var reactions: [Reaction]
public static func all() -> Request<[Announcement]> {
return Request(method: .get, path: "/api/v1/announcements")
}
public static func dismiss(id: String) -> Request<Empty> {
return Request(method: .post, path: "/api/v1/announcements/\(id)/dismiss")
}
public static func react(id: String, name: String) -> Request<Empty> {
return Request(method: .put, path: "/api/v1/announcements/\(id)/reactions/\(name)")
}
public static func unreact(id: String, name: String) -> Request<Empty> {
return Request(method: .delete, path: "/api/v1/announcements/\(id)/reactions/\(name)")
}
enum CodingKeys: String, CodingKey {
case id
case content
case startsAt = "starts_at"
case endsAt = "ends_at"
case allDay = "all_day"
case publishedAt = "published_at"
case updatedAt = "updated_at"
case read
case mentions
case statuses
case tags
case emojis
case reactions
}
}
extension Announcement {
public struct Account: Decodable, Sendable, Hashable {
public let id: String
public let username: String
public let url: WebURL
public let acct: String
}
}
extension Announcement {
public struct Status: Decodable, Sendable, Hashable {
public let id: String
public let url: WebURL
}
}
extension Announcement {
public struct Reaction: Decodable, Sendable, Hashable {
public let name: String
public var count: Int
public var me: Bool?
public let url: URL?
public let staticURL: URL?
public init(name: String, count: Int, me: Bool?, url: URL?, staticURL: URL?) {
self.name = name
self.count = count
self.me = me
self.url = url
self.staticURL = staticURL
}
enum CodingKeys: String, CodingKey {
case name
case count
case me
case url
case staticURL = "static_url"
}
}
}

View File

@ -43,8 +43,13 @@ extension Emoji: CustomDebugStringConvertible {
}
}
extension Emoji: Equatable {
extension Emoji: Equatable, Hashable {
public static func ==(lhs: Emoji, rhs: Emoji) -> Bool {
return lhs.shortcode == rhs.shortcode && lhs.url == rhs.url
}
public func hash(into hasher: inout Hasher) {
hasher.combine(shortcode)
hasher.combine(url)
}
}

View File

@ -227,6 +227,12 @@
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4662BD079800054DB14 /* AnnouncementsHostingController.swift */; };
D698F4692BD0799F0054DB14 /* AnnouncementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4682BD0799F0054DB14 /* AnnouncementsView.swift */; };
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46A2BD079F00054DB14 /* AnnouncementListRow.swift */; };
D698F46D2BD0B8310054DB14 /* AnnouncementsCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */; };
D698F46F2BD0B8DF0054DB14 /* AddReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */; };
D698F4712BD0CBAA0054DB14 /* AnnouncementContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */; };
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; };
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; };
@ -650,6 +656,12 @@
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
D698F4662BD079800054DB14 /* AnnouncementsHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsHostingController.swift; sourceTree = "<group>"; };
D698F4682BD0799F0054DB14 /* AnnouncementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsView.swift; sourceTree = "<group>"; };
D698F46A2BD079F00054DB14 /* AnnouncementListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementListRow.swift; sourceTree = "<group>"; };
D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsCollection.swift; sourceTree = "<group>"; };
D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReactionView.swift; sourceTree = "<group>"; };
D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnouncementContentTextView.swift; sourceTree = "<group>"; };
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = "<group>"; };
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = "<group>"; };
@ -1055,6 +1067,7 @@
children = (
D65B4B89297879DE00DABDFB /* Account Follows */,
D6A3BC822321F69400FD64D5 /* Account List */,
D698F4472BCEE2320054DB14 /* Announcements */,
D641C787213DD862004B4513 /* Compose */,
D641C785213DD83B004B4513 /* Conversation */,
D6F2E960249E772F005846BB /* Crash Reporter */,
@ -1350,6 +1363,19 @@
path = About;
sourceTree = "<group>";
};
D698F4472BCEE2320054DB14 /* Announcements */ = {
isa = PBXGroup;
children = (
D698F4662BD079800054DB14 /* AnnouncementsHostingController.swift */,
D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */,
D698F4682BD0799F0054DB14 /* AnnouncementsView.swift */,
D698F46A2BD079F00054DB14 /* AnnouncementListRow.swift */,
D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */,
D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */,
);
path = Announcements;
sourceTree = "<group>";
};
D6A3BC822321F69400FD64D5 /* Account List */ = {
isa = PBXGroup;
children = (
@ -2131,6 +2157,7 @@
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */,
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */,
@ -2161,8 +2188,10 @@
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
D698F4692BD0799F0054DB14 /* AnnouncementsView.swift in Sources */,
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
D698F46F2BD0B8DF0054DB14 /* AddReactionView.swift in Sources */,
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
@ -2190,6 +2219,7 @@
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */,
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */,
D61ABEF628EE74D400B29151 /* StatusCollectionViewCell.swift in Sources */,
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */,
D61DC84628F498F200B82C6E /* Logging.swift in Sources */,
@ -2325,6 +2355,7 @@
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
D698F46D2BD0B8310054DB14 /* AnnouncementsCollection.swift in Sources */,
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */,
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */,
D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */,
@ -2369,6 +2400,7 @@
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */,
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
D6C4532F2BCB873400E26A0E /* MockStatusView.swift in Sources */,
D698F4712BD0CBAA0054DB14 /* AnnouncementContentTextView.swift in Sources */,
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,

View File

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "face.smiling.badge.plus.svg",
"idiom" : "universal"
}
]
}

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 232.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="3300" height="2200">
<!--glyph: "", point size: 100.0, font version: "19.2d2e1", template writer version: "128"-->
<style>.monochrome-0 {-sfsymbols-motion-group:1}
.monochrome-1 {-sfsymbols-motion-group:1}
.monochrome-2 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
.monochrome-3 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
.monochrome-4 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
.multicolor-0:tintColor {-sfsymbols-motion-group:1}
.multicolor-1:tintColor {-sfsymbols-motion-group:1}
.multicolor-2:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
.multicolor-3:systemGreenColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
.multicolor-4:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
.hierarchical-0:secondary {-sfsymbols-motion-group:1}
.hierarchical-1:secondary {-sfsymbols-motion-group:1}
.hierarchical-2:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
.hierarchical-3:primary {-sfsymbols-motion-group:0}
.hierarchical-4:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
</style>
<g id="Notes">
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
<g transform="matrix(0.2 0 0 0.2 263 1933)">
<path d="m46.2402 4.15039c21.5332 0 39.4531-17.8711 39.4531-39.4043s-17.9688-39.4043-39.502-39.4043c-21.4844 0-39.3555 17.8711-39.3555 39.4043s17.9199 39.4043 39.4043 39.4043Zm0-7.42188c-17.7246 0-31.8848-14.209-31.8848-31.9824s14.1113-31.9824 31.8359-31.9824c17.7734 0 32.0312 14.209 32.0312 31.9824s-14.209 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
<path d="m58.5449 14.5508c27.2461 0 49.8047-22.6074 49.8047-49.8047 0-27.2461-22.6074-49.8047-49.8535-49.8047-27.1973 0-49.7559 22.5586-49.7559 49.8047 0 27.1973 22.6074 49.8047 49.8047 49.8047Zm0-8.30078c-23.0469 0-41.4551-18.457-41.4551-41.5039s18.3594-41.5039 41.4062-41.5039 41.5527 18.457 41.5527 41.5039-18.457 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
<path d="m74.8535 28.3203c34.8145 0 63.623-28.8086 63.623-63.5742 0-34.8145-28.8574-63.623-63.6719-63.623-34.7656 0-63.5254 28.8086-63.5254 63.623 0 34.7656 28.8086 63.5742 63.5742 63.5742Zm0-9.08203c-30.1758 0-54.4434-24.3164-54.4434-54.4922 0-30.2246 24.2188-54.4922 54.3945-54.4922 30.2246 0 54.541 24.2676 54.541 54.4922 0 30.1758-24.2676 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
<g transform="matrix(0.2 0 0 0.2 776 1933)">
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
</g>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.5.0</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 15 or greater</text>
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
</g>
<g id="Guides">
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="515.649" x2="515.649" y1="600.785" y2="720.121"/>
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="603.773" x2="603.773" y1="600.785" y2="720.121"/>
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1403.58" x2="1403.58" y1="600.785" y2="720.121"/>
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1496.11" x2="1496.11" y1="600.785" y2="720.121"/>
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2884.57" x2="2884.57" y1="600.785" y2="720.121"/>
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2982.23" x2="2982.23" y1="600.785" y2="720.121"/>
</g>
<g id="Symbols">
<g id="Black-S" transform="matrix(1 0 0 1 2884.57 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M48.8281 6.78711C71.9727 6.78711 90.8203-12.0605 90.8203-35.2051C90.8203-58.3496 71.9727-77.1973 48.8281-77.1973C25.6836-77.1973 6.83594-58.3496 6.83594-35.2051C6.83594-12.0605 25.6836 6.78711 48.8281 6.78711ZM48.8281-7.37305C33.4473-7.37305 20.9961-19.8242 20.9961-35.2051C20.9961-50.5859 33.4473-63.0371 48.8281-63.0371C64.209-63.0371 76.6602-50.5859 76.6602-35.2051C76.6602-19.8242 64.209-7.37305 48.8281-7.37305Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M48.8281-18.1641C56.9824-18.1641 62.4512-23.5352 62.4512-26.1719C62.4512-27.1973 61.4258-27.6855 60.4004-27.2461C57.4707-25.9766 54.3945-24.2676 48.8281-24.2676C43.2617-24.2676 40.0879-25.8789 37.2559-27.2461C36.2305-27.7344 35.2051-27.1973 35.2051-26.1719C35.2051-23.5352 40.625-18.1641 48.8281-18.1641ZM37.793-38.916C40.0879-38.916 42.0898-40.9668 42.0898-43.7988C42.0898-46.6797 40.0879-48.7305 37.793-48.7305C35.498-48.7305 33.5938-46.6797 33.5938-43.7988C33.5938-40.9668 35.498-38.916 37.793-38.916ZM59.8145-38.916C62.0605-38.916 64.1113-40.9668 64.1113-43.7988C64.1113-46.6797 62.0605-48.7305 59.8145-48.7305C57.4707-48.7305 55.5664-46.6797 55.5664-43.7988C55.5664-40.9668 57.4707-38.916 59.8145-38.916Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M87.8941 20.2949C102.836 20.2949 115.189 7.89255 115.189-7.04885C115.189-21.9903 102.836-34.2949 87.8941-34.2949C72.9527-34.2949 60.5992-21.9903 60.5992-7.04885C60.5992 7.89255 72.9527 20.2949 87.8941 20.2949Z"/>
<path class="monochrome-3 multicolor-3:systemGreenColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M87.8941 13.9473C99.3688 13.9473 108.842 4.37695 108.842-7.04885C108.842-18.4746 99.3688-27.9472 87.8941-27.9472C76.4195-27.9472 66.9468-18.4746 66.9468-7.04885C66.9468 4.37695 76.4195 13.9473 87.8941 13.9473Z"/>
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M87.8941 7.01365C85.5503 7.01365 83.9878 5.45115 83.9878 3.15625L83.9878-3.09375L77.8355-3.09375C75.5406-3.09375 73.9292-4.65625 73.9292-7.00005C73.9292-9.34375 75.4429-10.9062 77.8355-10.9062L83.9878-10.9062L83.9878-17.0097C83.9878-19.3047 85.5503-20.9161 87.8941-20.9161C90.2378-20.9161 91.8003-19.4023 91.8003-17.0097L91.8003-10.9062L98.0018-10.9062C100.297-10.9062 101.859-9.34375 101.859-7.00005C101.859-4.65625 100.297-3.09375 98.0018-3.09375L91.8003-3.09375L91.8003 3.15625C91.8003 5.45115 90.2378 7.01365 87.8941 7.01365Z"/>
</g>
<g id="Regular-S" transform="matrix(1 0 0 1 1403.58 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M46.2402 4.15039C67.7734 4.15039 85.6934-13.7207 85.6934-35.2539C85.6934-56.7871 67.7246-74.6582 46.1914-74.6582C24.707-74.6582 6.83594-56.7871 6.83594-35.2539C6.83594-13.7207 24.7559 4.15039 46.2402 4.15039ZM46.2402-3.27148C28.5156-3.27148 14.3555-17.4805 14.3555-35.2539C14.3555-53.0273 28.4668-67.2363 46.1914-67.2363C63.9648-67.2363 78.2227-53.0273 78.2227-35.2539C78.2227-17.4805 64.0137-3.27148 46.2402-3.27148Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M46.1914-15.8691C54.3457-15.8691 59.8145-21.2402 59.8145-23.877C59.8145-24.8535 58.8379-25.3418 57.8613-24.9512C54.9805-23.584 51.8066-21.875 46.1914-21.875C40.625-21.875 37.4512-23.584 34.5703-24.9512C33.5938-25.3418 32.6172-24.8535 32.6172-23.877C32.6172-21.2402 38.0859-15.8691 46.1914-15.8691ZM34.9121-38.5742C37.4512-38.5742 39.6973-40.7715 39.6973-43.9453C39.6973-47.2168 37.4512-49.4141 34.9121-49.4141C32.4219-49.4141 30.2246-47.2168 30.2246-43.9453C30.2246-40.7715 32.4219-38.5742 34.9121-38.5742ZM57.5195-38.5742C60.0586-38.5742 62.2559-40.7715 62.2559-43.9453C62.2559-47.2168 60.0586-49.4141 57.5195-49.4141C54.9805-49.4141 52.832-47.2168 52.832-43.9453C52.832-40.7715 54.9805-38.5742 57.5195-38.5742Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M83.277 18.3906C97.0956 18.3906 108.668 6.8184 108.668-7C108.668-20.916 97.1926-32.3906 83.277-32.3906C69.3121-32.3906 57.8864-20.916 57.8864-7C57.8864 6.9649 69.3121 18.3906 83.277 18.3906Z"/>
<path class="monochrome-3 multicolor-3:systemGreenColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M83.277 12.6777C93.9216 12.6777 102.955 3.7422 102.955-7C102.955-17.791 94.0676-26.6777 83.277-26.6777C72.486-26.6777 63.5504-17.791 63.5504-7C63.5504 3.8399 72.486 12.6777 83.277 12.6777Z"/>
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M83.277 5.2559C81.7145 5.2559 80.7379 4.2305 80.7379 2.7168L80.7379-4.4609L73.5602-4.4609C72.0465-4.4609 71.0211-5.4863 71.0211-7C71.0211-8.5137 72.0465-9.5391 73.5602-9.5391L80.7379-9.5391L80.7379-16.7168C80.7379-18.2305 81.7145-19.2559 83.277-19.2559C84.7906-19.2559 85.816-18.2305 85.816-16.7168L85.816-9.5391L92.9936-9.5391C94.5076-9.5391 95.4836-8.5137 95.4836-7C95.4836-5.4863 94.5076-4.4609 92.9936-4.4609L85.816-4.4609L85.816 2.7168C85.816 4.2305 84.7906 5.2559 83.277 5.2559Z"/>
</g>
<g id="Ultralight-S" transform="matrix(1 0 0 1 515.649 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M44.0606 1.97072C64.5039 1.97072 81.2886-14.8105 81.2886-35.2539C81.2886-55.6973 64.5005-72.4785 44.0571-72.4785C23.5718-72.4785 6.83594-55.6973 6.83594-35.2539C6.83594-14.8105 23.5752 1.97072 44.0606 1.97072ZM44.0606-0.274438C24.7466-0.274438 9.04252-15.9365 9.04252-35.2539C9.04252-54.5713 24.7432-70.2334 44.0571-70.2334C63.3745-70.2334 79.04-54.5713 79.04-35.2539C79.04-15.9365 63.3779-0.274438 44.0606-0.274438Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M44.0571-17.0044C51.3032-17.0044 56.3633-22.103 56.3633-24.2856C56.3633-24.7627 55.9317-24.8423 55.6362-24.5879C53.4365-22.4487 49.4453-20.6489 44.0571-20.6489C38.6724-20.6489 34.7266-22.4941 32.4815-24.5879C32.186-24.8423 31.7544-24.7627 31.7544-24.2856C31.7544-22.103 36.8145-17.0044 44.0571-17.0044ZM32.5054-39.0283C34.4541-39.0283 36.2007-41.0439 36.2007-43.5366C36.2007-45.9907 34.4995-48.0064 32.5054-48.0064C30.5147-48.0064 28.8169-45.9907 28.8169-43.5366C28.8169-41.0439 30.5601-39.0283 32.5054-39.0283ZM55.6123-39.0283C57.5611-39.0283 59.3042-41.0439 59.3042-43.5366C59.3042-45.9907 57.6065-48.0064 55.6123-48.0064C53.6182-48.0064 51.9238-45.9907 51.9238-43.5366C51.9238-41.0439 53.6636-39.0283 55.6123-39.0283Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M79.3341 14.3492C90.9727 14.3492 100.684 4.72955 100.684-6.99995C100.684-18.7362 91.0247-28.3492 79.3341-28.3492C67.6397-28.3492 57.9395-18.6908 57.9395-6.99995C57.9395 4.73985 67.6397 14.3492 79.3341 14.3492Z"/>
<path class="monochrome-3 multicolor-3:systemGreenColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M79.3341 11.2701C89.2977 11.2701 97.6037 3.06105 97.6037-6.99995C97.6037-17.0644 89.3527-25.2699 79.3341-25.2699C69.315-25.2699 61.0152-17.019 61.0152-6.99995C61.0152 3.06795 69.315 11.2701 79.3341 11.2701Z"/>
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M79.3341 4.89265C78.3619 4.89265 77.794 4.23055 77.794 3.35255L77.794-5.50535L68.9361-5.50535C68.149-5.50535 67.4415-6.03115 67.4415-6.99995C67.4415-7.96875 68.149-8.54005 68.9361-8.54005L77.794-8.54005L77.794-17.3071C77.794-18.1396 78.3619-18.8471 79.3341-18.8471C80.2574-18.8471 80.8287-18.1396 80.8287-17.3071L80.8287-8.54005L89.6407-8.54005C90.4737-8.54005 91.1327-7.96875 91.1327-6.99995C91.1327-6.03115 90.4737-5.50535 89.6407-5.50535L80.8287-5.50535L80.8287 3.35255C80.8287 4.23055 80.2574 4.89265 79.3341 4.89265Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,191 @@
//
// AddReactionView.swift
// Tusker
//
// Created by Shadowfacts on 4/17/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import Pachyderm
import TuskerComponents
struct AddReactionView: View {
let mastodonController: MastodonController
let addReaction: (Reaction) async throws -> Void
@Environment(\.dismiss) private var dismiss
@ScaledMetric private var emojiSize = 30
@State private var allEmojis: [Emoji] = []
@State private var emojisBySection: [String: [Emoji]] = [:]
@State private var query = ""
@State private var error: (any Error)?
var body: some View {
NavigationView {
ScrollView(.vertical) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: emojiSize), spacing: 4)]) {
if query.count == 1 {
Section {
AddReactionButton {
await doAddReaction(.emoji(query))
} label: {
Text(query)
.font(.system(size: 25))
}
.buttonStyle(.plain)
}
}
ForEach(emojisBySection.keys.sorted(), id: \.self) { section in
Section {
ForEach(emojisBySection[section]!, id: \.shortcode) { emoji in
AddReactionButton {
await doAddReaction(.custom(emoji))
} label: {
CustomEmojiImageView(emoji: emoji)
.frame(height: emojiSize)
.accessibilityLabel(emoji.shortcode)
}
}
} header: {
if !section.isEmpty {
VStack(alignment: .leading, spacing: 2) {
Text(section)
.font(.caption)
Divider()
}
.padding(.top, 4)
}
}
}
}
.padding(.horizontal)
}
.searchable(text: $query, placement: .navigationBarDrawer(displayMode: .always))
.searchPresentationToolbarBehaviorIfAvailable()
.onChange(of: query) { _ in
updateFilteredEmojis()
}
.navigationTitle("Add Reaction")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(role: .cancel) {
dismiss()
} label: {
Text("Cancel")
}
}
}
}
.navigationViewStyle(.stack)
.mediumPresentationDetentIfAvailable()
.alertWithData("Error Adding Reaction", data: $error, actions: { _ in
Button("OK") {}
}, message: { error in
Text(error.localizedDescription)
})
.task {
allEmojis = await mastodonController.getCustomEmojis()
updateFilteredEmojis()
}
}
private func updateFilteredEmojis() {
let filteredEmojis = if !query.isEmpty {
allEmojis.map { emoji -> (Emoji, (matched: Bool, score: Int)) in
(emoji, FuzzyMatcher.match(pattern: query, str: emoji.shortcode))
}
.filter(\.1.matched)
.sorted { $0.1.score > $1.1.score }
.map(\.0)
} else {
allEmojis
}
var shortcodes = Set<String>()
var newEmojis = [Emoji]()
var newEmojisBySection = [String: [Emoji]]()
for emoji in filteredEmojis where !shortcodes.contains(emoji.shortcode) {
newEmojis.append(emoji)
shortcodes.insert(emoji.shortcode)
let category = emoji.category ?? ""
if newEmojisBySection.keys.contains(category) {
newEmojisBySection[category]!.append(emoji)
} else {
newEmojisBySection[category] = [emoji]
}
}
emojisBySection = newEmojisBySection
}
private func doAddReaction(_ reaction: Reaction) async {
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
do {
try await addReaction(reaction)
dismiss()
} catch {
self.error = error
}
}
enum Reaction {
case emoji(String)
case custom(Emoji)
}
}
private struct AddReactionButton<Label: View>: View {
let addReaction: () async -> Void
@ViewBuilder let label: Label
@State private var isLoading = false
var body: some View {
Button {
isLoading = true
Task {
await addReaction()
isLoading = false
}
} label: {
ZStack {
label
.opacity(isLoading ? 0 : 1)
if isLoading {
ProgressView()
}
}
}
.padding(2)
.hoverEffect()
}
}
private extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func mediumPresentationDetentIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self.presentationDetents([.medium, .large])
} else {
self
}
}
@available(iOS, obsoleted: 17.1)
@ViewBuilder
func searchPresentationToolbarBehaviorIfAvailable() -> some View {
if #available(iOS 17.1, *) {
self.searchPresentationToolbarBehavior(.avoidHidingContent)
} else {
self
}
}
}
//#Preview {
// AddReactionView()
//}

View File

@ -0,0 +1,45 @@
//
// AnnouncementContentTextView.swift
// Tusker
//
// Created by Shadowfacts on 4/16/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import WebURL
class AnnouncementContentTextView: ContentTextView {
var heightChanged: ((CGFloat) -> Void)?
private var announcement: Announcement?
override func layoutSubviews() {
super.layoutSubviews()
heightChanged?(contentSize.height)
}
func setTextFrom(announcement: Announcement, content: NSAttributedString) {
self.announcement = announcement
self.attributedText = content
setEmojis(announcement.emojis, identifier: announcement.id)
}
override func getMention(for url: URL, text: String) -> Mention? {
announcement?.mentions.first {
URL($0.url) == url
}.map {
Mention(url: $0.url, username: $0.username, acct: $0.acct, id: $0.id)
}
}
override func getHashtag(for url: URL, text: String) -> Hashtag? {
announcement?.tags.first {
URL($0.url) == url
}
}
}

View File

@ -0,0 +1,246 @@
//
// AnnouncementListRow.swift
// Tusker
//
// Created by Shadowfacts on 4/17/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import Pachyderm
import TuskerComponents
import WebURLFoundationExtras
struct AnnouncementListRow: View {
@Binding var announcement: Announcement
let mastodonController: MastodonController
let navigationDelegate: TuskerNavigationDelegate?
let removeAnnouncement: @MainActor () -> Void
@State private var contentTextViewHeight: CGFloat?
@State private var isShowingAddReactionSheet = false
var body: some View {
if #available(iOS 16.0, *) {
mostOfTheBody
.alignmentGuide(.listRowSeparatorLeading, computeValue: { dimension in
dimension[.leading]
})
} else {
mostOfTheBody
}
}
private var mostOfTheBody: some View {
VStack {
HStack(alignment: .top) {
AnnouncementContentTextViewRepresentable(announcement: announcement, navigationDelegate: navigationDelegate) { newHeight in
DispatchQueue.main.async {
contentTextViewHeight = newHeight
}
}
.frame(height: contentTextViewHeight)
Text(announcement.publishedAt, format: .abbreviatedTimeAgo)
.fontWeight(.light)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 16)
ScrollView(.horizontal) {
LazyHStack {
Button {
isShowingAddReactionSheet = true
} label: {
Label {
Text("Add Reaction")
} icon: {
if #available(iOS 16.0, *) {
Image("face.smiling.badge.plus")
} else {
Image(systemName: "face.smiling")
}
}
}
.labelStyle(.iconOnly)
.padding(4)
.hoverEffect()
ForEach($announcement.reactions, id: \.name) { $reaction in
ReactionButton(announcement: announcement, reaction: $reaction, mastodonController: mastodonController)
}
}
.frame(height: 32)
.padding(.horizontal, 16)
}
}
.padding(.vertical, 8)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.swipeActions {
Button(role: .destructive) {
Task {
await dismissAnnouncement()
}
} label: {
Text("Dismiss")
}
}
.contextMenu {
Button(role: .destructive) {
Task {
await dismissAnnouncement()
await removeAnnouncement()
}
} label: {
Label("Dismiss", systemImage: "xmark")
}
}
.sheet(isPresented: $isShowingAddReactionSheet) {
AddReactionView(mastodonController: mastodonController, addReaction: self.addReaction)
}
}
private func dismissAnnouncement() async {
do {
_ = try await mastodonController.run(Announcement.dismiss(id: announcement.id))
} catch {
Logging.general.error("Error dismissing attachment: \(String(describing: error))")
}
}
@MainActor
private func addReaction(_ reaction: AddReactionView.Reaction) async throws {
let name = switch reaction {
case .emoji(let s): s
case .custom(let emoji): emoji.shortcode
}
_ = try await mastodonController.run(Announcement.react(id: announcement.id, name: name))
for (idx, reaction) in announcement.reactions.enumerated() {
if reaction.name == name {
announcement.reactions[idx].me = true
announcement.reactions[idx].count += 1
return
}
}
let url: URL?
let staticURL: URL?
if case .custom(let emoji) = reaction {
url = URL(emoji.url)
staticURL = URL(emoji.staticURL)
} else {
url = nil
staticURL = nil
}
announcement.reactions.append(.init(name: name, count: 1, me: true, url: url, staticURL: staticURL))
}
}
private struct AnnouncementContentTextViewRepresentable: UIViewRepresentable {
let announcement: Announcement
let navigationDelegate: TuskerNavigationDelegate?
let heightChanged: (CGFloat) -> Void
func makeUIView(context: Context) -> AnnouncementContentTextView {
let view = AnnouncementContentTextView()
view.isScrollEnabled = true
view.backgroundColor = .clear
view.isEditable = false
view.isSelectable = false
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.adjustsFontForContentSizeCategory = true
return view
}
func updateUIView(_ uiView: AnnouncementContentTextView, context: Context) {
uiView.navigationDelegate = navigationDelegate
uiView.setTextFrom(announcement: announcement, content: TimelineStatusCollectionViewCell.htmlConverter.convert(announcement.content))
uiView.heightChanged = heightChanged
}
}
private struct ReactionButton: View {
let announcement: Announcement
@Binding var reaction: Announcement.Reaction
let mastodonController: MastodonController
@State private var customEmojiImage: (Image, CGFloat)?
var body: some View {
Button(action: self.toggleReaction) {
let countStr = reaction.count.formatted(.number)
let title = if reaction.name.count == 1 {
"\(reaction.name) \(countStr)"
} else {
countStr
}
if reaction.url != nil {
Label {
Text(title)
} icon: {
if let (image, aspectRatio) = customEmojiImage {
image.aspectRatio(aspectRatio, contentMode: .fit)
}
}
} else {
Text(title)
}
}
.buttonStyle(TintedButtonStyle(highlighted: reaction.me == true))
.font(.body.monospacedDigit())
.hoverEffect()
.task {
if let url = reaction.url,
let image = await ImageCache.emojis.get(url).1 {
let aspectRatio = image.size.width / image.size.height
customEmojiImage = (
Image(uiImage: image).resizable(),
aspectRatio
)
}
}
}
private func toggleReaction() {
if reaction.me == true {
let oldCount = reaction.count
reaction.me = false
reaction.count -= 1
Task {
do {
_ = try await mastodonController.run(Announcement.unreact(id: announcement.id, name: reaction.name))
} catch {
reaction.me = true
reaction.count = oldCount
}
}
} else {
let oldCount = reaction.count
reaction.me = true
reaction.count += 1
Task {
do {
_ = try await mastodonController.run(Announcement.react(id: announcement.id, name: reaction.name))
} catch {
reaction.me = false
reaction.count = oldCount
}
}
}
}
}
private struct TintedButtonStyle: ButtonStyle {
let highlighted: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(highlighted ? AnyShapeStyle(.white) : AnyShapeStyle(.tint))
.padding(.vertical, 4)
.padding(.horizontal, 8)
.frame(height: 32)
.background(.tint.opacity(highlighted ? 1 : 0.2), in: RoundedRectangle(cornerRadius: 4))
.opacity(configuration.isPressed ? 0.8 : 1)
}
}
//#Preview {
// AnnouncementListRow()
//}

View File

@ -0,0 +1,18 @@
//
// AnnouncementsCollection.swift
// Tusker
//
// Created by Shadowfacts on 4/17/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
class AnnouncementsCollection: ObservableObject {
@Published var announcements: [Announcement]
init(announcements: [Announcement]) {
self.announcements = announcements
}
}

View File

@ -0,0 +1,32 @@
//
// AnnouncementsHostingController.swift
// Tusker
//
// Created by Shadowfacts on 4/17/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import Pachyderm
class AnnouncementsHostingController: UIHostingController<AnnouncementsView> {
private let mastodonController: MastodonController
init(announcements: AnnouncementsCollection, mastodonController: MastodonController) {
self.mastodonController = mastodonController
@Box var boxedSelf: TuskerNavigationDelegate?
super.init(rootView: AnnouncementsView(announcements: announcements, mastodonController: mastodonController, navigationDelegate: _boxedSelf))
boxedSelf = self
navigationItem.title = "Announcements"
}
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension AnnouncementsHostingController: TuskerNavigationDelegate {
nonisolated var apiController: MastodonController! { mastodonController }
}

View File

@ -0,0 +1,39 @@
//
// AnnouncementsView.swift
// Tusker
//
// Created by Shadowfacts on 4/17/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import SwiftUI
import Pachyderm
struct AnnouncementsView: View {
@ObservedObject var state: AnnouncementsCollection
let mastodonController: MastodonController
@Box var navigationDelegate: TuskerNavigationDelegate?
init(announcements: AnnouncementsCollection, mastodonController: MastodonController, navigationDelegate: Box<TuskerNavigationDelegate?>) {
self.state = announcements
self.mastodonController = mastodonController
self._navigationDelegate = navigationDelegate
}
var body: some View {
List {
ForEach($state.announcements) { $announcement in
AnnouncementListRow(announcement: $announcement, mastodonController: mastodonController, navigationDelegate: navigationDelegate) {
withAnimation {
state.announcements.removeAll(where: { $0.id == announcement.id })
}
}
}
}
.listStyle(.grouped)
}
}
//#Preview {
// AnnouncementsView()
//}

View File

@ -16,6 +16,22 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
var initialMode: NotificationsMode?
private lazy var announcementsButton: UIButton = {
#if os(visionOS)
var config = UIButton.Configuration.borderedProminent()
#else
var config = UIButton.Configuration.plain()
// We don't want a background for this button, even when accessibility button shapes are enabled, because it's in the navbar.
config.background.backgroundColor = .clear
#endif
config.image = UIImage(systemName: "megaphone.fill")
config.contentInsets = .zero
let button = UIButton(configuration: config)
button.addTarget(self, action: #selector(announcementsButtonPresesd), for: .touchUpInside)
return button
}()
private var unreadAnnouncements: AnnouncementsCollection?
init(initialMode: NotificationsMode? = nil, mastodonController: MastodonController) {
self.initialMode = initialMode
self.mastodonController = mastodonController
@ -30,6 +46,8 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
title = Page.all.title
tabBarItem.image = UIImage(systemName: "bell.fill")
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: announcementsButton)
announcementsButton.isHidden = true
}
required init?(coder: NSCoder) {
@ -42,6 +60,14 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
selectMode(initialMode ?? Preferences.shared.defaultNotificationsMode)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Task {
await checkForAnnouncements()
}
}
func selectMode(_ mode: NotificationsMode) {
let page: Page
switch mode {
@ -53,6 +79,61 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
selectPage(page, animated: false)
}
private func checkForAnnouncements() async {
guard mastodonController.instanceFeatures.instanceAnnouncements else {
navigationItem.rightBarButtonItem = nil
return
}
let announcements: [Announcement]
do {
(announcements, _) = try await mastodonController.run(Announcement.all())
} catch {
Logging.general.error("Error fetching announcements: \(String(describing: error))")
return
}
let unread = announcements.filter { $0.read == false }
if unread.isEmpty {
unreadAnnouncements = nil
if #available(iOS 17.0, *) {
announcementsButton.imageView!.addSymbolEffect(.disappear)
} else {
UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseInOut) {
self.announcementsButton.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
self.announcementsButton.layer.opacity = 0
} completion: { _ in
self.announcementsButton.transform = .identity
self.announcementsButton.layer.opacity = 1
}
}
} else {
announcementsButton.isHidden = false
announcementsButton.layer.opacity = 1
unreadAnnouncements = AnnouncementsCollection(announcements: unread)
if #available(iOS 17.0, *) {
// make sure to remove the .disappear effect, which stays around indefinitely
announcementsButton.imageView!.removeAllSymbolEffects()
announcementsButton.imageView!.addSymbolEffect(.bounce)
} else {
UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseInOut) {
self.announcementsButton.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
} completion: { _ in
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
self.announcementsButton.transform = .identity
}
}
}
}
}
@objc private func announcementsButtonPresesd() {
guard let unreadAnnouncements else {
return
}
show(AnnouncementsHostingController(announcements: unreadAnnouncements, mastodonController: mastodonController), sender: nil)
}
}
extension NotificationsPageViewController {
enum Page: SegmentedPageViewControllerPage {
case all
case mentions