247 lines
8.4 KiB
Swift
247 lines
8.4 KiB
Swift
//
|
|
// 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()
|
|
//}
|