SwiftFormat

This commit is contained in:
Thomas Ricouard 2023-12-18 08:22:59 +01:00
parent 2145bd5971
commit 8ff3e22d9f
55 changed files with 472 additions and 450 deletions

View file

@ -10,11 +10,11 @@ extension IceCubesApp {
} }
.keyboardShortcut("n", modifiers: .shift) .keyboardShortcut("n", modifiers: .shift)
Button("menu.new-post") { Button("menu.new-post") {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
#else #else
sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
#endif #endif
} }
.keyboardShortcut("n", modifiers: .command) .keyboardShortcut("n", modifiers: .command)
} }

View file

@ -96,7 +96,7 @@ extension IceCubesApp {
} }
.defaultSize(width: 600, height: 800) .defaultSize(width: 600, height: 800)
.windowResizability(.contentMinSize) .windowResizability(.contentMinSize)
WindowGroup(for: WindowDestinationMedia.self) { destination in WindowGroup(for: WindowDestinationMedia.self) { destination in
Group { Group {
switch destination.wrappedValue { switch destination.wrappedValue {

View file

@ -1,9 +1,9 @@
import DesignSystem import DesignSystem
import Env import Env
import Models
import Observation import Observation
import SafariServices import SafariServices
import SwiftUI import SwiftUI
import Models
extension View { extension View {
@MainActor func withSafariRouter() -> some View { @MainActor func withSafariRouter() -> some View {
@ -43,9 +43,9 @@ private struct SafariRouter: ViewModifier {
return .handled return .handled
} }
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction } guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
#endif #endif
// SFSafariViewController only supports initial URLs with http:// or https:// schemes. // SFSafariViewController only supports initial URLs with http:// or https:// schemes.
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else { guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
return .systemAction return .systemAction

View file

@ -57,11 +57,11 @@ struct SideBarView<Content: View>: View {
private var postButton: some View { private var postButton: some View {
Button { Button {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)) openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
#else #else
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility) routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
#endif #endif
} label: { } label: {
Image(systemName: "square.and.pencil") Image(systemName: "square.and.pencil")
.resizable() .resizable()

View file

@ -1,9 +1,9 @@
import Account
import DesignSystem import DesignSystem
import Env import Env
import SwiftUI
import Account
import Network
import Models import Models
import Network
import SwiftUI
@MainActor @MainActor
struct AboutView: View { struct AboutView: View {
@ -13,7 +13,7 @@ struct AboutView: View {
@State private var dimillianAccount: AccountsListRowViewModel? @State private var dimillianAccount: AccountsListRowViewModel?
@State private var iceCubesAccount: AccountsListRowViewModel? @State private var iceCubesAccount: AccountsListRowViewModel?
let versionNumber: String let versionNumber: String
init() { init() {
@ -59,7 +59,6 @@ struct AboutView: View {
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
followAccountsSection followAccountsSection
Section { Section {
@ -109,8 +108,7 @@ struct AboutView: View {
routerPath.handle(url: url) routerPath.handle(url: url)
}) })
} }
@ViewBuilder @ViewBuilder
private var followAccountsSection: some View { private var followAccountsSection: some View {
if let iceCubesAccount, let dimillianAccount { if let iceCubesAccount, let dimillianAccount {
@ -132,18 +130,18 @@ struct AboutView: View {
group.addTask { group.addTask {
let viewModel = try await fetchAccountViewModel(account: "dimillian@mastodon.social") let viewModel = try await fetchAccountViewModel(account: "dimillian@mastodon.social")
await MainActor.run { await MainActor.run {
self.dimillianAccount = viewModel dimillianAccount = viewModel
} }
} }
group.addTask { group.addTask {
let viewModel = try await fetchAccountViewModel(account: "icecubesapp@mastodon.online") let viewModel = try await fetchAccountViewModel(account: "icecubesapp@mastodon.online")
await MainActor.run { await MainActor.run {
self.iceCubesAccount = viewModel iceCubesAccount = viewModel
} }
} }
} }
} }
private func fetchAccountViewModel(account: String) async throws -> AccountsListRowViewModel { private func fetchAccountViewModel(account: String) async throws -> AccountsListRowViewModel {
let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account)) let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account))
let rel: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [dimillianAccount.id])) let rel: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [dimillianAccount.id]))

View file

@ -218,7 +218,8 @@ struct AddAccountView: View {
signInClient = .init(server: sanitizedName) signInClient = .init(server: sanitizedName)
if let oauthURL = try? await signInClient?.oauthURL(), if let oauthURL = try? await signInClient?.oauthURL(),
let url = try? await webAuthenticationSession.authenticate(using: oauthURL, let url = try? await webAuthenticationSession.authenticate(using: oauthURL,
callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: "")){ callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: ""))
{
await continueSignIn(url: url) await continueSignIn(url: url)
} else { } else {
isSigninIn = false isSigninIn = false

View file

@ -212,7 +212,7 @@ struct DisplaySettingsView: View {
Double(userPreferences.maxReplyIndentation) Double(userPreferences.maxReplyIndentation)
}, set: { newVal in }, set: { newVal in
userPreferences.maxReplyIndentation = UInt(newVal) userPreferences.maxReplyIndentation = UInt(newVal)
}), in: 1...20, step: 1) }), in: 1 ... 20, step: 1)
Text("settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))") Text("settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))")
.font(.scaledBody) .font(.scaledBody)
} }

View file

@ -53,7 +53,7 @@ struct IconSelectorView: View {
static let items = [ static let items = [
IconSelector(title: "settings.app.icon.official".localized, icons: [.primary, .alt1, .alt2, .alt3, .alt4, .alt5, .alt6, .alt7, .alt8, IconSelector(title: "settings.app.icon.official".localized, icons: [.primary, .alt1, .alt2, .alt3, .alt4, .alt5, .alt6, .alt7, .alt8,
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt9, .alt10, .alt11, .alt12, .alt13, .alt14,
.alt15, .alt16, .alt17, .alt18, .alt19, .alt25, .alt15, .alt16, .alt17, .alt18, .alt19, .alt25,
.alt43, .alt44, .alt45, .alt46, .alt47]), .alt43, .alt44, .alt45, .alt46, .alt47]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt20, .alt21, .alt22, .alt23, .alt24]), IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt20, .alt21, .alt22, .alt23, .alt24]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt26, .alt27, .alt28]), IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt26, .alt27, .alt28]),

View file

@ -29,7 +29,7 @@ struct SettingsTabs: View {
@State private var timelineCache = TimelineCache() @State private var timelineCache = TimelineCache()
@Binding var popToRootTab: Tab @Binding var popToRootTab: Tab
let isModal: Bool let isModal: Bool
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline] @Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
@ -160,10 +160,10 @@ struct SettingsTabs: View {
Label("settings.general.translate", systemImage: "captions.bubble") Label("settings.general.translate", systemImage: "captions.bubble")
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
Link(destination: URL(string: UIApplication.openSettingsURLString)!) { Link(destination: URL(string: UIApplication.openSettingsURLString)!) {
Label("settings.system", systemImage: "gear") Label("settings.system", systemImage: "gear")
} }
.tint(theme.labelColor) .tint(theme.labelColor)
#endif #endif
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
@ -173,24 +173,24 @@ struct SettingsTabs: View {
private var otherSections: some View { private var otherSections: some View {
@Bindable var preferences = preferences @Bindable var preferences = preferences
Section("settings.section.other") { Section("settings.section.other") {
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
Picker(selection: $preferences.preferredBrowser) { Picker(selection: $preferences.preferredBrowser) {
ForEach(PreferredBrowser.allCases, id: \.rawValue) { browser in ForEach(PreferredBrowser.allCases, id: \.rawValue) { browser in
switch browser { switch browser {
case .inAppSafari: case .inAppSafari:
Text("settings.general.browser.in-app").tag(browser) Text("settings.general.browser.in-app").tag(browser)
case .safari: case .safari:
Text("settings.general.browser.system").tag(browser) Text("settings.general.browser.system").tag(browser)
}
} }
} label: {
Label("settings.general.browser", systemImage: "network")
} }
} label: { Toggle(isOn: $preferences.inAppBrowserReaderView) {
Label("settings.general.browser", systemImage: "network") Label("settings.general.browser.in-app.readerview", systemImage: "doc.plaintext")
} }
Toggle(isOn: $preferences.inAppBrowserReaderView) { .disabled(preferences.preferredBrowser != PreferredBrowser.inAppSafari)
Label("settings.general.browser.in-app.readerview", systemImage: "doc.plaintext") #endif
}
.disabled(preferences.preferredBrowser != PreferredBrowser.inAppSafari)
#endif
Toggle(isOn: $preferences.isOpenAIEnabled) { Toggle(isOn: $preferences.isOpenAIEnabled) {
Label("settings.other.hide-openai", systemImage: "faxmachine") Label("settings.other.hide-openai", systemImage: "faxmachine")
} }
@ -209,19 +209,19 @@ struct SettingsTabs: View {
private var appSection: some View { private var appSection: some View {
Section { Section {
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
NavigationLink(destination: IconSelectorView()) { NavigationLink(destination: IconSelectorView()) {
Label { Label {
Text("settings.app.icon") Text("settings.app.icon")
} icon: { } icon: {
let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon") let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon")
Image(uiImage: .init(named: icon.iconName)!) Image(uiImage: .init(named: icon.iconName)!)
.resizable() .resizable()
.frame(width: 25, height: 25) .frame(width: 25, height: 25)
.cornerRadius(4) .cornerRadius(4)
}
} }
} #endif
#endif
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) { Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) {
Label("settings.app.source", systemImage: "link") Label("settings.app.source", systemImage: "link")

View file

@ -21,9 +21,9 @@ enum Tab: Int, Identifiable, Hashable {
static func loggedInTabs() -> [Tab] { static func loggedInTabs() -> [Tab] {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
return [.timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .settings] [.timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .settings]
} else { } else {
return [.timeline, .notifications, .explore, .messages, .profile] [.timeline, .notifications, .explore, .messages, .profile]
} }
} }

View file

@ -4,10 +4,10 @@ import Env
import Models import Models
import Network import Network
import NukeUI import NukeUI
import Shimmer
import SwiftUI
import SwiftData
import SFSafeSymbols import SFSafeSymbols
import Shimmer
import SwiftData
import SwiftUI
@MainActor @MainActor
struct EditTagGroupView: View { struct EditTagGroupView: View {
@ -53,8 +53,8 @@ struct EditTagGroupView: View {
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle( .navigationTitle(
isNewGroup isNewGroup
? "timeline.filter.add-tag-groups" ? "timeline.filter.add-tag-groups"
: "timeline.filter.edit-tag-groups" : "timeline.filter.edit-tag-groups"
) )
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
@ -76,9 +76,9 @@ struct EditTagGroupView: View {
} }
init(tagGroup: TagGroup = .emptyGroup(), onSaved: ((TagGroup) -> Void)? = nil) { init(tagGroup: TagGroup = .emptyGroup(), onSaved: ((TagGroup) -> Void)? = nil) {
self._tagGroup = State(wrappedValue: tagGroup) _tagGroup = State(wrappedValue: tagGroup)
self.onSaved = onSaved self.onSaved = onSaved
self.isNewGroup = tagGroup.title.isEmpty isNewGroup = tagGroup.title.isEmpty
} }
private func save() { private func save() {
@ -320,7 +320,7 @@ private struct SymbolSearchResultsView: View {
.onAppear { .onAppear {
results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol) results = TagGroup.searchSymbol(for: symbolQuery, exclude: selectedSymbol)
} }
case .invalid(let description): case let .invalid(description):
Text(description) Text(description)
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -335,6 +335,7 @@ private struct SymbolSearchResultsView: View {
} }
// MARK: search results validation // MARK: search results validation
enum ValidationStatus: Equatable { enum ValidationStatus: Equatable {
case valid case valid
case invalid(description: LocalizedStringKey) case invalid(description: LocalizedStringKey)
@ -342,22 +343,23 @@ private struct SymbolSearchResultsView: View {
var validationStatus: ValidationStatus { var validationStatus: ValidationStatus {
if results.isEmpty { if results.isEmpty {
if symbolQuery == selectedSymbol if symbolQuery == selectedSymbol,
&& !symbolQuery.isEmpty !symbolQuery.isEmpty,
&& results.count == 0 results.count == 0
{ {
return .invalid(description: "\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected") .invalid(description: "\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected")
} else { } else {
return .invalid(description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found") .invalid(description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found")
} }
} else { } else {
return .valid .valid
} }
} }
} }
extension TagGroup { extension TagGroup {
// MARK: title validation // MARK: title validation
enum TitleValidationStatus: Equatable { enum TitleValidationStatus: Equatable {
case valid case valid
case invalid(description: LocalizedStringKey) case invalid(description: LocalizedStringKey)
@ -365,11 +367,12 @@ extension TagGroup {
var titleValidationStatus: TitleValidationStatus { var titleValidationStatus: TitleValidationStatus {
title.isEmpty title.isEmpty
? .invalid(description: "add-tag-groups.edit.title.field.warning.empty-title") ? .invalid(description: "add-tag-groups.edit.title.field.warning.empty-title")
: .valid : .valid
} }
// MARK: symbolName validation // MARK: symbolName validation
enum SymbolNameValidationStatus: Equatable { enum SymbolNameValidationStatus: Equatable {
case valid case valid
case invalid(description: LocalizedStringKey) case invalid(description: LocalizedStringKey)
@ -386,6 +389,7 @@ extension TagGroup {
} }
// MARK: tags validation // MARK: tags validation
enum TagsValidationStatus: Equatable { enum TagsValidationStatus: Equatable {
case valid case valid
case invalid(description: LocalizedStringKey) case invalid(description: LocalizedStringKey)
@ -399,19 +403,22 @@ extension TagGroup {
} }
// MARK: TagGroup validation // MARK: TagGroup validation
var isValid: Bool { var isValid: Bool {
titleValidationStatus == .valid titleValidationStatus == .valid
&& symbolNameValidationStatus == .valid && symbolNameValidationStatus == .valid
&& tagsValidationStatus == .valid && tagsValidationStatus == .valid
} }
// MARK: format // MARK: format
func format() { func format() {
title = title.trimmingCharacters(in: .whitespacesAndNewlines) title = title.trimmingCharacters(in: .whitespacesAndNewlines)
tags = tags.map { $0.lowercased() } tags = tags.map { $0.lowercased() }
} }
// MARK: static members // MARK: static members
static func emptyGroup() -> TagGroup { static func emptyGroup() -> TagGroup {
TagGroup(title: "", symbolName: "", tags: []) TagGroup(title: "", symbolName: "", tags: [])
} }
@ -419,9 +426,9 @@ extension TagGroup {
static func searchSymbol(for query: String, exclude excludedSymbol: String) -> [String] { static func searchSymbol(for query: String, exclude excludedSymbol: String) -> [String] {
guard !query.isEmpty else { return [] } guard !query.isEmpty else { return [] }
return Self.allSymbols.filter { return allSymbols.filter {
$0.contains(query) && $0.contains(query) &&
$0 != excludedSymbol $0 != excludedSymbol
} }
} }
@ -432,7 +439,7 @@ extension TagGroup {
extension Text { extension Text {
func warningLabel() -> Text { func warningLabel() -> Text {
self.font(.caption) font(.caption)
.foregroundStyle(.red) .foregroundStyle(.red)
} }
} }

View file

@ -81,13 +81,14 @@ struct AccountDetailHeaderView: View {
return return
} }
let attachement = MediaAttachment.imageWith(url: account.header) let attachement = MediaAttachment.imageWith(url: account.header)
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationMedia.mediaViewer( openWindow(value: WindowDestinationMedia.mediaViewer(
attachments: [attachement], attachments: [attachement],
selectedAttachment: attachement)) selectedAttachment: attachement
#else ))
#else
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement]) quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
#endif #endif
} }
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
.accessibilityAddTraits([.isImage, .isButton]) .accessibilityAddTraits([.isImage, .isButton])
@ -117,12 +118,12 @@ struct AccountDetailHeaderView: View {
return return
} }
let attachement = MediaAttachment.imageWith(url: account.avatar) let attachement = MediaAttachment.imageWith(url: account.avatar)
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement], openWindow(value: WindowDestinationMedia.mediaViewer(attachments: [attachement],
selectedAttachment: attachement)) selectedAttachment: attachement))
#else #else
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement]) quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
#endif #endif
} }
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
.accessibilityAddTraits([.isImage, .isButton]) .accessibilityAddTraits([.isImage, .isButton])

View file

@ -1,7 +1,7 @@
import DesignSystem import DesignSystem
import Env
import Models import Models
import Network import Network
import Env
import SwiftUI import SwiftUI
@MainActor @MainActor
@ -16,7 +16,7 @@ public struct EditAccountView: View {
public init() {} public init() {}
public var body: some View { public var body: some View {
NavigationStack{ NavigationStack {
Form { Form {
if viewModel.isLoading { if viewModel.isLoading {
loadingSection loadingSection

View file

@ -6,21 +6,21 @@ public extension Font {
// See https://gist.github.com/zacwest/916d31da5d03405809c4 for iOS values // See https://gist.github.com/zacwest/916d31da5d03405809c4 for iOS values
// Custom values for Mac // Custom values for Mac
private static let title = 28.0 private static let title = 28.0
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
private static let headline = 20.0 private static let headline = 20.0
private static let body = 19.0 private static let body = 19.0
private static let callout = 17.0 private static let callout = 17.0
private static let subheadline = 16.0 private static let subheadline = 16.0
private static let footnote = 15.0 private static let footnote = 15.0
private static let caption = 14.0 private static let caption = 14.0
#else #else
private static let headline = 17.0 private static let headline = 17.0
private static let body = 17.0 private static let body = 17.0
private static let callout = 16.0 private static let callout = 16.0
private static let subheadline = 15.0 private static let subheadline = 15.0
private static let footnote = 13.0 private static let footnote = 13.0
private static let caption = 12.0 private static let caption = 12.0
#endif #endif
private static func customFont(size: CGFloat, relativeTo textStyle: TextStyle) -> Font { private static func customFont(size: CGFloat, relativeTo textStyle: TextStyle) -> Font {
if let chosenFont = Theme.shared.chosenFont { if let chosenFont = Theme.shared.chosenFont {

View file

@ -6,14 +6,14 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
public var window: UIWindow? public var window: UIWindow?
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
public func scene(_ scene: UIScene, public func scene(_ scene: UIScene,
willConnectTo _: UISceneSession, willConnectTo _: UISceneSession,
options _: UIScene.ConnectionOptions) options _: UIScene.ConnectionOptions)
{ {
guard let windowScene = scene as? UIWindowScene else { return } guard let windowScene = scene as? UIWindowScene else { return }
window = windowScene.keyWindow window = windowScene.keyWindow
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
if let titlebar = windowScene.titlebar { if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden titlebar.titleVisibility = .hidden
@ -22,10 +22,10 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
#endif #endif
} }
public override init() { override public init() {
super.init() super.init()
self.windowWidth = self.window?.bounds.size.width ?? UIScreen.main.bounds.size.width windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
self.windowHeight = self.window?.bounds.size.height ?? UIScreen.main.bounds.size.height windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
Self.observedSceneDelegate.insert(self) Self.observedSceneDelegate.insert(self)
_ = Self.observer // just for activating the lazy static property _ = Self.observer // just for activating the lazy static property
} }

View file

@ -1,9 +1,9 @@
import Env import Env
import Models
import Nuke import Nuke
import NukeUI import NukeUI
import Shimmer import Shimmer
import SwiftUI import SwiftUI
import Models
struct AccountPopoverView: View { struct AccountPopoverView: View {
let account: Account let account: Account
@ -36,7 +36,7 @@ struct AccountPopoverView: View {
} }
.frame(height: adaptiveConfig.height / 2, alignment: .bottom) .frame(height: adaptiveConfig.height / 2, alignment: .bottom)
EmojiTextApp(.init(stringValue: account.safeDisplayName ), emojis: account.emojis) EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
.font(.headline) .font(.headline)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
.emojiSize(Font.scaledHeadlineFont.emojiSize) .emojiSize(Font.scaledHeadlineFont.emojiSize)
@ -122,11 +122,10 @@ struct AccountPopoverView: View {
} }
private var adaptiveConfig: AvatarView.FrameConfig { private var adaptiveConfig: AvatarView.FrameConfig {
var cornerRadius: CGFloat var cornerRadius: CGFloat = if config == .badge || theme.avatarShape == .circle {
if config == .badge || theme.avatarShape == .circle { config.width / 2
cornerRadius = config.width / 2
} else { } else {
cornerRadius = config.cornerRadius config.cornerRadius
} }
return AvatarView.FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius) return AvatarView.FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius)
} }
@ -142,7 +141,7 @@ extension VerticalAlignment {
static let bottomAvatar = VerticalAlignment(BottomAvatarAlignment.self) static let bottomAvatar = VerticalAlignment(BottomAvatarAlignment.self)
} }
public struct AccountPopoverModifier : ViewModifier { public struct AccountPopoverModifier: ViewModifier {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@ -156,7 +155,7 @@ public struct AccountPopoverModifier : ViewModifier {
if !userPreferences.showAccountPopover { if !userPreferences.showAccountPopover {
return AnyView(content) return AnyView(content)
} }
return AnyView(content return AnyView(content
.onHover { hovering in .onHover { hovering in
if hovering { if hovering {
@ -191,8 +190,8 @@ public struct AccountPopoverModifier : ViewModifier {
} }
} }
extension View { public extension View {
public func accountPopover(_ account: Account) -> some View { func accountPopover(_ account: Account) -> some View {
modifier(AccountPopoverModifier(account)) modifier(AccountPopoverModifier(account))
} }
} }

View file

@ -1,8 +1,8 @@
import Models
import Nuke import Nuke
import NukeUI import NukeUI
import Shimmer import Shimmer
import SwiftUI import SwiftUI
import Models
@MainActor @MainActor
public struct AvatarView: View { public struct AvatarView: View {
@ -21,11 +21,10 @@ public struct AvatarView: View {
} }
private var adaptiveConfig: FrameConfig { private var adaptiveConfig: FrameConfig {
var cornerRadius: CGFloat var cornerRadius: CGFloat = if config == .badge || theme.avatarShape == .circle {
if config == .badge || theme.avatarShape == .circle { config.width / 2
cornerRadius = config.width / 2
} else { } else {
cornerRadius = config.cornerRadius config.cornerRadius
} }
return FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius) return FrameConfig(width: config.width, height: config.height, cornerRadius: cornerRadius)
} }
@ -42,16 +41,16 @@ public struct AvatarView: View {
let cornerRadius: CGFloat let cornerRadius: CGFloat
init(width: CGFloat, height: CGFloat, cornerRadius: CGFloat = 4) { init(width: CGFloat, height: CGFloat, cornerRadius: CGFloat = 4) {
self.size = CGSize(width: width, height: height) size = CGSize(width: width, height: height)
self.cornerRadius = cornerRadius self.cornerRadius = cornerRadius
} }
public static let account = FrameConfig(width: 80, height: 80) public static let account = FrameConfig(width: 80, height: 80)
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
public static let status = FrameConfig(width: 48, height: 48) public static let status = FrameConfig(width: 48, height: 48)
#else #else
public static let status = FrameConfig(width: 40, height: 40) public static let status = FrameConfig(width: 40, height: 40)
#endif #endif
public static let embed = FrameConfig(width: 34, height: 34) public static let embed = FrameConfig(width: 34, height: 34)
public static let badge = FrameConfig(width: 28, height: 28, cornerRadius: 14) public static let badge = FrameConfig(width: 28, height: 28, cornerRadius: 14)
public static let list = FrameConfig(width: 20, height: 20, cornerRadius: 10) public static let list = FrameConfig(width: 20, height: 20, cornerRadius: 10)
@ -77,10 +76,10 @@ struct PreviewWrapper: View {
Toggle("Avatar Shape", isOn: $isCircleAvatar) Toggle("Avatar Shape", isOn: $isCircleAvatar)
} }
.onChange(of: isCircleAvatar) { .onChange(of: isCircleAvatar) {
Theme.shared.avatarShape = self.isCircleAvatar ? .circle : .rounded Theme.shared.avatarShape = isCircleAvatar ? .circle : .rounded
} }
.onAppear { .onAppear {
Theme.shared.avatarShape = self.isCircleAvatar ? .circle : .rounded Theme.shared.avatarShape = isCircleAvatar ? .circle : .rounded
} }
} }
@ -103,7 +102,8 @@ struct PreviewWrapper: View {
url: URL(string: "https://nondot.org/sabre/")!, url: URL(string: "https://nondot.org/sabre/")!,
source: nil, source: nil,
bot: false, bot: false,
discoverable: true) discoverable: true
)
} }
struct AvatarImage: View { struct AvatarImage: View {

View file

@ -26,12 +26,12 @@ public struct StatusEditorToolbarItem: ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
Task { @MainActor in Task { @MainActor in
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: visibility)) openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: visibility))
#else #else
routerPath.presentedSheet = .newStatusEditor(visibility: visibility) routerPath.presentedSheet = .newStatusEditor(visibility: visibility)
HapticManager.shared.fireHaptic(.buttonPress) HapticManager.shared.fireHaptic(.buttonPress)
#endif #endif
} }
} label: { } label: {
Image(systemName: "square.and.pencil") Image(systemName: "square.and.pencil")

View file

@ -1,21 +1,21 @@
import SwiftUI
import Charts import Charts
import Models import Models
import SwiftUI
public struct TagChartView: View { public struct TagChartView: View {
@State private var sortedHistory: [Tag.History] = [] @State private var sortedHistory: [Tag.History] = []
public init(tag: Tag) { public init(tag: Tag) {
_sortedHistory = .init(initialValue: tag.history.sorted { _sortedHistory = .init(initialValue: tag.history.sorted {
Int($0.day) ?? 0 < Int($1.day) ?? 0 Int($0.day) ?? 0 < Int($1.day) ?? 0
}) })
} }
public var body: some View { public var body: some View {
Chart(sortedHistory) { data in Chart(sortedHistory) { data in
AreaMark(x: .value("day", sortedHistory.firstIndex(where: { $0.id == data.id }) ?? 0), AreaMark(x: .value("day", sortedHistory.firstIndex(where: { $0.id == data.id }) ?? 0),
y: .value("uses", Int(data.uses) ?? 0)) y: .value("uses", Int(data.uses) ?? 0))
.interpolationMethod(.catmullRom) .interpolationMethod(.catmullRom)
} }
.chartLegend(.hidden) .chartLegend(.hidden)
.chartXAxis(.hidden) .chartXAxis(.hidden)

View file

@ -87,7 +87,7 @@ import Observation
tags = [] tags = []
} }
} }
public func deleteList(list: Models.List) async { public func deleteList(list: Models.List) async {
guard let client else { return } guard let client else { return }
lists.removeAll(where: { $0.id == list.id }) lists.removeAll(where: { $0.id == list.id })

View file

@ -68,7 +68,7 @@ public extension EnvironmentValues {
get { self[IndentationLevel.self] } get { self[IndentationLevel.self] }
set { self[IndentationLevel.self] = newValue } set { self[IndentationLevel.self] = newValue }
} }
var isHomeTimeline: Bool { var isHomeTimeline: Bool {
get { self[IsHomeTimeline.self] } get { self[IsHomeTimeline.self] }
set { self[IsHomeTimeline.self] = newValue } set { self[IsHomeTimeline.self] = newValue }

View file

@ -1,9 +1,9 @@
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Env
import Models import Models
import Network import Network
import SwiftUI import SwiftUI
import Env
@MainActor @MainActor
public struct ListCreateView: View { public struct ListCreateView: View {
@ -11,13 +11,13 @@ public struct ListCreateView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(Client.self) private var client @Environment(Client.self) private var client
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@State private var title = "" @State private var title = ""
@State private var repliesPolicy: Models.List.RepliesPolicy = .list @State private var repliesPolicy: Models.List.RepliesPolicy = .list
@State private var isExclusive: Bool = false @State private var isExclusive: Bool = false
@State private var isSaving: Bool = false @State private var isSaving: Bool = false
public init() { } public init() {}
public var body: some View { public var body: some View {
NavigationStack { NavigationStack {
@ -25,7 +25,8 @@ public struct ListCreateView: View {
Section("lists.edit.settings") { Section("lists.edit.settings") {
TextField("list.edit.title", text: $title) TextField("list.edit.title", text: $title)
Picker("list.edit.repliesPolicy", Picker("list.edit.repliesPolicy",
selection: $repliesPolicy) { selection: $repliesPolicy)
{
ForEach(Models.List.RepliesPolicy.allCases) { policy in ForEach(Models.List.RepliesPolicy.allCases) { policy in
Text(policy.title) Text(policy.title)
.tag(policy) .tag(policy)
@ -43,8 +44,8 @@ public struct ListCreateView: View {
Task { Task {
isSaving = true isSaving = true
let _: Models.List = try await client.post(endpoint: Lists.createList(title: title, let _: Models.List = try await client.post(endpoint: Lists.createList(title: title,
repliesPolicy: repliesPolicy, repliesPolicy: repliesPolicy,
exclusive: isExclusive )) exclusive: isExclusive))
await currentAccount.fetchLists() await currentAccount.fetchLists()
isSaving = false isSaving = false
dismiss() dismiss()

View file

@ -1,9 +1,9 @@
import Account
import DesignSystem import DesignSystem
import EmojiText import EmojiText
import Models import Models
import Network import Network
import SwiftUI import SwiftUI
import Account
@MainActor @MainActor
public struct ListEditView: View { public struct ListEditView: View {
@ -22,10 +22,11 @@ public struct ListEditView: View {
Form { Form {
Section("lists.edit.settings") { Section("lists.edit.settings") {
TextField("list.edit.title", text: $viewModel.title) { TextField("list.edit.title", text: $viewModel.title) {
Task { await viewModel.update() } Task { await viewModel.update() }
} }
Picker("list.edit.repliesPolicy", Picker("list.edit.repliesPolicy",
selection: $viewModel.repliesPolicy) { selection: $viewModel.repliesPolicy)
{
ForEach(Models.List.RepliesPolicy.allCases) { policy in ForEach(Models.List.RepliesPolicy.allCases) { policy in
Text(policy.title) Text(policy.title)
.tag(policy) .tag(policy)
@ -41,7 +42,7 @@ public struct ListEditView: View {
.onChange(of: viewModel.isExclusive) { _, _ in .onChange(of: viewModel.isExclusive) { _, _ in
Task { await viewModel.update() } Task { await viewModel.update() }
} }
Section("lists.edit.users-in-list") { Section("lists.edit.users-in-list") {
HStack { HStack {
TextField("lists.edit.users-search", TextField("lists.edit.users-search",
@ -91,7 +92,7 @@ public struct ListEditView: View {
} }
} }
} }
private var loadingView: some View { private var loadingView: some View {
HStack { HStack {
Spacer() Spacer()
@ -100,7 +101,7 @@ public struct ListEditView: View {
} }
.id(UUID()) .id(UUID())
} }
@ViewBuilder @ViewBuilder
private var searchAccountsView: some View { private var searchAccountsView: some View {
if viewModel.isSearching { if viewModel.isSearching {
@ -138,15 +139,15 @@ public struct ListEditView: View {
relationship: relationship, relationship: relationship,
shouldDisplayNotify: false, shouldDisplayNotify: false,
relationshipUpdated: { relationship in relationshipUpdated: { relationship in
viewModel.searchedRelationships[account.id] = relationship viewModel.searchedRelationships[account.id] = relationship
})) }))
} }
} }
} }
} }
} }
} }
@ViewBuilder @ViewBuilder
private var listAccountsView: some View { private var listAccountsView: some View {
if viewModel.isLoadingAccounts { if viewModel.isLoadingAccounts {
@ -176,5 +177,4 @@ public struct ListEditView: View {
} }
} }
} }
} }

View file

@ -1,9 +1,9 @@
import Combine import Combine
import Env
import Models import Models
import Network import Network
import Observation import Observation
import SwiftUI import SwiftUI
import Env
@MainActor @MainActor
@Observable public class ListEditViewModel { @Observable public class ListEditViewModel {
@ -13,13 +13,13 @@ import Env
var isLoadingAccounts: Bool = true var isLoadingAccounts: Bool = true
var accounts: [Account] = [] var accounts: [Account] = []
var title: String var title: String
var repliesPolicy: Models.List.RepliesPolicy var repliesPolicy: Models.List.RepliesPolicy
var isExclusive: Bool var isExclusive: Bool
var isUpdating: Bool = false var isUpdating: Bool = false
var searchUserQuery: String = "" var searchUserQuery: String = ""
var searchedAccounts: [Account] = [] var searchedAccounts: [Account] = []
var searchedRelationships: [String: Relationship] = [:] var searchedRelationships: [String: Relationship] = [:]
@ -27,9 +27,9 @@ import Env
init(list: Models.List) { init(list: Models.List) {
self.list = list self.list = list
self.title = list.title title = list.title
self.repliesPolicy = list.repliesPolicy ?? .list repliesPolicy = list.repliesPolicy ?? .list
self.isExclusive = list.exclusive ?? false isExclusive = list.exclusive ?? false
} }
func fetchAccounts() async { func fetchAccounts() async {
@ -42,27 +42,27 @@ import Env
isLoadingAccounts = false isLoadingAccounts = false
} }
} }
func update() async { func update() async {
guard let client else { return } guard let client else { return }
do { do {
isUpdating = true isUpdating = true
let list: Models.List = try await client.put(endpoint: let list: Models.List = try await client.put(endpoint:
Lists.updateList(id: list.id, Lists.updateList(id: list.id,
title: title, title: title,
repliesPolicy: repliesPolicy, repliesPolicy: repliesPolicy,
exclusive: isExclusive )) exclusive: isExclusive))
self.list = list self.list = list
self.title = list.title title = list.title
self.repliesPolicy = list.repliesPolicy ?? .list repliesPolicy = list.repliesPolicy ?? .list
self.isExclusive = list.exclusive ?? false isExclusive = list.exclusive ?? false
self.isUpdating = false isUpdating = false
await CurrentAccount.shared.fetchLists() await CurrentAccount.shared.fetchLists()
} catch { } catch {
isUpdating = false isUpdating = false
} }
} }
func add(account: Account) async { func add(account: Account) async {
guard let client else { return } guard let client else { return }
do { do {
@ -76,7 +76,7 @@ import Env
isUpdating = false isUpdating = false
} }
} }
func delete(account: Account) async { func delete(account: Account) async {
guard let client else { return } guard let client else { return }
do { do {
@ -90,7 +90,7 @@ import Env
isUpdating = false isUpdating = false
} }
} }
func searchUsers() async { func searchUsers() async {
guard let client, !searchUserQuery.isEmpty else { return } guard let client, !searchUserQuery.isEmpty else { return }
do { do {
@ -107,7 +107,7 @@ import Env
} }
searchedAccounts = results.accounts searchedAccounts = results.accounts
isSearching = false isSearching = false
} catch { } } catch {}
} }
} }
@ -115,11 +115,11 @@ extension Models.List.RepliesPolicy {
var title: LocalizedStringKey { var title: LocalizedStringKey {
switch self { switch self {
case .followed: case .followed:
return "list.repliesPolicy.followed" "list.repliesPolicy.followed"
case .list: case .list:
return "list.repliesPolicy.list" "list.repliesPolicy.list"
case .none: case .none:
return "list.repliesPolicy.none" "list.repliesPolicy.none"
} }
} }
} }

View file

@ -54,7 +54,7 @@ public struct MediaUIView: View, @unchecked Sendable {
data = attachments.compactMap { DisplayData(from: $0) } data = attachments.compactMap { DisplayData(from: $0) }
initialItem = DisplayData(from: selectedAttachment) initialItem = DisplayData(from: selectedAttachment)
} }
private func scrollToPrevious() { private func scrollToPrevious() {
if let scrolledItem, let index = data.firstIndex(of: scrolledItem), index > 0 { if let scrolledItem, let index = data.firstIndex(of: scrolledItem), index > 0 {
withAnimation { withAnimation {
@ -62,9 +62,9 @@ public struct MediaUIView: View, @unchecked Sendable {
} }
} }
} }
private func scrollToNext() { private func scrollToNext() {
if let scrolledItem, let index = data.firstIndex(of: scrolledItem), index < data.count - 1{ if let scrolledItem, let index = data.firstIndex(of: scrolledItem), index < data.count - 1 {
withAnimation { withAnimation {
self.scrolledItem = data[index + 1] self.scrolledItem = data[index + 1]
} }

View file

@ -5,13 +5,13 @@ public struct List: Codable, Identifiable, Equatable, Hashable {
public let title: String public let title: String
public let repliesPolicy: RepliesPolicy? public let repliesPolicy: RepliesPolicy?
public let exclusive: Bool? public let exclusive: Bool?
public enum RepliesPolicy: String, Sendable, Codable, CaseIterable, Identifiable { public enum RepliesPolicy: String, Sendable, Codable, CaseIterable, Identifiable {
public var id: String { public var id: String {
rawValue rawValue
} }
case followed, list, `none` case followed, list, none
} }
} }

View file

@ -5,6 +5,7 @@ public struct Tag: Codable, Identifiable, Equatable, Hashable {
public var id: String { public var id: String {
day day
} }
public let day: String public let day: String
public let accounts: String public let accounts: String
public let uses: String public let uses: String

View file

@ -14,7 +14,7 @@ public enum Apps: Endpoint {
public func queryItems() -> [URLQueryItem]? { public func queryItems() -> [URLQueryItem]? {
switch self { switch self {
case .registerApp: case .registerApp:
return [ [
.init(name: "client_name", value: AppInfo.clientName), .init(name: "client_name", value: AppInfo.clientName),
.init(name: "redirect_uris", value: AppInfo.scheme), .init(name: "redirect_uris", value: AppInfo.scheme),
.init(name: "scopes", value: AppInfo.scopes), .init(name: "scopes", value: AppInfo.scopes),

View file

@ -35,14 +35,14 @@ public enum Oauth: Endpoint {
public func queryItems() -> [URLQueryItem]? { public func queryItems() -> [URLQueryItem]? {
switch self { switch self {
case let .authorize(clientId): case let .authorize(clientId):
return [ [
.init(name: "response_type", value: "code"), .init(name: "response_type", value: "code"),
.init(name: "client_id", value: clientId), .init(name: "client_id", value: clientId),
.init(name: "redirect_uri", value: AppInfo.scheme), .init(name: "redirect_uri", value: AppInfo.scheme),
.init(name: "scope", value: AppInfo.scopes), .init(name: "scope", value: AppInfo.scopes),
] ]
default: default:
return nil nil
} }
} }
} }

View file

@ -29,10 +29,10 @@ public enum ServerFilters: Endpoint {
public func queryItems() -> [URLQueryItem]? { public func queryItems() -> [URLQueryItem]? {
switch self { switch self {
case let .addKeyword(_, keyword, wholeWord): case let .addKeyword(_, keyword, wholeWord):
return [.init(name: "keyword", value: keyword), [.init(name: "keyword", value: keyword),
.init(name: "whole_word", value: wholeWord ? "true" : "false")] .init(name: "whole_word", value: wholeWord ? "true" : "false")]
default: default:
return nil nil
} }
} }

View file

@ -35,18 +35,19 @@ public struct OpenAIClient {
self.temperature = temperature self.temperature = temperature
} }
} }
public struct VisionRequest: OpenAIRequest { public struct VisionRequest: OpenAIRequest {
public struct Message: Encodable { public struct Message: Encodable {
public struct MessageContent: Encodable { public struct MessageContent: Encodable {
public struct ImageUrl: Encodable { public struct ImageUrl: Encodable {
public let url: URL public let url: URL
} }
public let type: String public let type: String
public let text: String? public let text: String?
public let imageUrl: ImageUrl? public let imageUrl: ImageUrl?
} }
public let role = "user" public let role = "user"
public let content: [MessageContent] public let content: [MessageContent]
} }
@ -77,8 +78,8 @@ public struct OpenAIClient {
case let .emphasize(input): case let .emphasize(input):
ChatRequest(content: "Make this text catchy, more fun: \(input)", temperature: 1) ChatRequest(content: "Make this text catchy, more fun: \(input)", temperature: 1)
case let .imageDescription(image): case let .imageDescription(image):
VisionRequest(messages: [.init(content: [.init(type: "text", text: "Whats in this image? Be brief, it's for image alt description on a social network. Don't write in the first person.", imageUrl: nil) VisionRequest(messages: [.init(content: [.init(type: "text", text: "Whats in this image? Be brief, it's for image alt description on a social network. Don't write in the first person.", imageUrl: nil),
, .init(type: "image_url", text: nil, imageUrl: .init(url: image))])]) .init(type: "image_url", text: nil, imageUrl: .init(url: image))])])
} }
} }
} }

View file

@ -54,8 +54,8 @@ import SwiftUI
} }
func loadSelectedType() { func loadSelectedType() {
self.client = client client = client
guard let value = UserDefaults.standard.string(forKey: filterKey) guard let value = UserDefaults.standard.string(forKey: filterKey)
else { else {
selectedType = nil selectedType = nil

View file

@ -114,7 +114,8 @@ import SwiftUI
indentationLevelPreviousCache = [:] indentationLevelPreviousCache = [:]
for status in statuses { for status in statuses {
if let inReplyToId = status.inReplyToId, if let inReplyToId = status.inReplyToId,
let prevIndent = indentationLevelPreviousCache[inReplyToId] { let prevIndent = indentationLevelPreviousCache[inReplyToId]
{
indentationLevelPreviousCache[status.id] = prevIndent + 1 indentationLevelPreviousCache[status.id] = prevIndent + 1
} else { } else {
indentationLevelPreviousCache[status.id] = 0 indentationLevelPreviousCache[status.id] = 0
@ -137,14 +138,14 @@ import SwiftUI
} }
} }
} }
func getIndentationLevel(id: String, maxIndent: UInt) -> (indentationLevel: UInt, extraInset: Double) { func getIndentationLevel(id: String, maxIndent: UInt) -> (indentationLevel: UInt, extraInset: Double) {
let level = min(indentationLevelPreviousCache[id] ?? 0, maxIndent) let level = min(indentationLevelPreviousCache[id] ?? 0, maxIndent)
let barSize = Double(level) * 2 let barSize = Double(level) * 2
let spaceBetween = (Double(level) - 1) * 3 let spaceBetween = (Double(level) - 1) * 3
let size = barSize + spaceBetween + 8 let size = barSize + spaceBetween + 8
return (level, size) return (level, size)
} }
} }

View file

@ -1,49 +1,49 @@
import SwiftUI
import GiphyUISDK
import UIKit
import DesignSystem import DesignSystem
import GiphyUISDK
import SwiftUI
import UIKit
struct GifPickerView: UIViewControllerRepresentable { struct GifPickerView: UIViewControllerRepresentable {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
var completion: ((String) -> Void) var completion: (String) -> Void
var onShouldDismissGifPicker: () -> Void var onShouldDismissGifPicker: () -> Void
func makeUIViewController(context: Context) -> GiphyViewController { func makeUIViewController(context: Context) -> GiphyViewController {
Giphy.configure(apiKey: "MIylJkNX57vcUNZxmSODKU9dQKBgXCkV") Giphy.configure(apiKey: "MIylJkNX57vcUNZxmSODKU9dQKBgXCkV")
let controller = GiphyViewController() let controller = GiphyViewController()
controller.swiftUIEnabled = true controller.swiftUIEnabled = true
controller.mediaTypeConfig = [.gifs, .stickers, .recents] controller.mediaTypeConfig = [.gifs, .stickers, .recents]
controller.delegate = context.coordinator controller.delegate = context.coordinator
controller.navigationController?.isNavigationBarHidden = true controller.navigationController?.isNavigationBarHidden = true
controller.navigationController?.setNavigationBarHidden(true, animated: false) controller.navigationController?.setNavigationBarHidden(true, animated: false)
GiphyViewController.trayHeightMultiplier = 1.0 GiphyViewController.trayHeightMultiplier = 1.0
controller.theme = GPHTheme(type: theme.selectedScheme == .dark ? .darkBlur : .lightBlur) controller.theme = GPHTheme(type: theme.selectedScheme == .dark ? .darkBlur : .lightBlur)
return controller return controller
} }
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } func updateUIViewController(_: UIViewControllerType, context _: Context) {}
func makeCoordinator() -> Coordinator { func makeCoordinator() -> Coordinator {
GifPickerView.Coordinator(parent: self) GifPickerView.Coordinator(parent: self)
} }
class Coordinator: NSObject, GiphyDelegate { class Coordinator: NSObject, GiphyDelegate {
var parent: GifPickerView var parent: GifPickerView
init(parent: GifPickerView) { init(parent: GifPickerView) {
self.parent = parent self.parent = parent
} }
func didDismiss(controller: GiphyViewController?) { func didDismiss(controller _: GiphyViewController?) {
self.parent.onShouldDismissGifPicker() parent.onShouldDismissGifPicker()
} }
func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) { func didSelectMedia(giphyViewController _: GiphyViewController, media: GPHMedia) {
let url = media.url(rendition: .fixedWidth, fileType: .gif) let url = media.url(rendition: .fixedWidth, fileType: .gif)
parent.completion(url ?? "") parent.completion(url ?? "")
} }

View file

@ -1,10 +1,10 @@
import DesignSystem import DesignSystem
import Env import Env
import GiphyUISDK
import Models import Models
import NukeUI import NukeUI
import PhotosUI import PhotosUI
import SwiftUI import SwiftUI
import GiphyUISDK
@MainActor @MainActor
struct StatusEditorAccessoryView: View { struct StatusEditorAccessoryView: View {
@ -41,19 +41,19 @@ struct StatusEditorAccessoryView: View {
} label: { } label: {
Label("status.editor.photo-library", systemImage: "photo") Label("status.editor.photo-library", systemImage: "photo")
} }
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
Button { Button {
isCameraPickerPresented = true isCameraPickerPresented = true
} label: { } label: {
Label("status.editor.camera-picker", systemImage: "camera") Label("status.editor.camera-picker", systemImage: "camera")
} }
#endif #endif
Button { Button {
isFileImporterPresented = true isFileImporterPresented = true
} label: { } label: {
Label("status.editor.browse-file", systemImage: "folder") Label("status.editor.browse-file", systemImage: "folder")
} }
Button { Button {
isGIFPickerPresented = true isGIFPickerPresented = true
} label: { } label: {
@ -70,8 +70,7 @@ struct StatusEditorAccessoryView: View {
selection: $viewModel.mediaPickers, selection: $viewModel.mediaPickers,
maxSelectionCount: 4, maxSelectionCount: 4,
matching: .any(of: [.images, .videos]), matching: .any(of: [.images, .videos]),
photoLibrary: .shared() photoLibrary: .shared())
)
.fileImporter(isPresented: $isFileImporterPresented, .fileImporter(isPresented: $isFileImporterPresented,
allowedContentTypes: [.image, .video], allowedContentTypes: [.image, .video],
allowsMultipleSelection: true) allowsMultipleSelection: true)
@ -92,7 +91,7 @@ struct StatusEditorAccessoryView: View {
}) })
.sheet(isPresented: $isGIFPickerPresented, content: { .sheet(isPresented: $isGIFPickerPresented, content: {
GifPickerView { url in GifPickerView { url in
GPHCache.shared.downloadAssetData(url) { data, error in GPHCache.shared.downloadAssetData(url) { data, _ in
guard let data else { return } guard let data else { return }
viewModel.processGIFData(data: data) viewModel.processGIFData(data: data)
} }
@ -109,7 +108,7 @@ struct StatusEditorAccessoryView: View {
// all SEVM have the same visibility value // all SEVM have the same visibility value
followUpSEVMs.append(StatusEditorViewModel(mode: .new(visibility: focusedSEVM.visibility))) followUpSEVMs.append(StatusEditorViewModel(mode: .new(visibility: focusedSEVM.visibility)))
} label: { } label: {
Image(systemName: "arrowshape.turn.up.left.circle.fill") Image(systemName: "arrowshape.turn.up.left.circle.fill")
} }
.disabled(!canAddNewSEVM) .disabled(!canAddNewSEVM)
@ -213,8 +212,8 @@ struct StatusEditorAccessoryView: View {
private var canAddNewSEVM: Bool { private var canAddNewSEVM: Bool {
guard followUpSEVMs.count < 5 else { return false } guard followUpSEVMs.count < 5 else { return false }
if followUpSEVMs.isEmpty, // there is only mainSEVM on the editor if followUpSEVMs.isEmpty, // there is only mainSEVM on the editor
!focusedSEVM.statusText.string.isEmpty // focusedSEVM is also mainSEVM !focusedSEVM.statusText.string.isEmpty // focusedSEVM is also mainSEVM
{ return true } { return true }
if let lastSEVMs = followUpSEVMs.last, if let lastSEVMs = followUpSEVMs.last,

View file

@ -2,7 +2,7 @@ import Foundation
import Models import Models
struct StatusEditorCategorizedEmojiContainer: Identifiable, Equatable { struct StatusEditorCategorizedEmojiContainer: Identifiable, Equatable {
let id = UUID().uuidString let id = UUID().uuidString
let categoryName: String let categoryName: String
var emojis: [Emoji] var emojis: [Emoji]
} }

View file

@ -1,9 +1,9 @@
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
import Network
import Shimmer import Shimmer
import SwiftUI import SwiftUI
import Network
@MainActor @MainActor
struct StatusEditorMediaEditView: View { struct StatusEditorMediaEditView: View {
@ -11,7 +11,7 @@ struct StatusEditorMediaEditView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
@Environment(CurrentInstance.self) private var currentInstance @Environment(CurrentInstance.self) private var currentInstance
@Environment(UserPreferences.self) private var preferences @Environment(UserPreferences.self) private var preferences
var viewModel: StatusEditorViewModel var viewModel: StatusEditorViewModel
let container: StatusEditorMediaContainer let container: StatusEditorMediaContainer
@ -22,7 +22,7 @@ struct StatusEditorMediaEditView: View {
@State private var didAppear: Bool = false @State private var didAppear: Bool = false
@State private var isGeneratingDescription: Bool = false @State private var isGeneratingDescription: Bool = false
@State private var showTranslateButton: Bool = false @State private var showTranslateButton: Bool = false
@State private var isTranslating: Bool = false @State private var isTranslating: Bool = false
@ -108,7 +108,7 @@ struct StatusEditorMediaEditView: View {
.preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light) .preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light)
} }
} }
@ViewBuilder @ViewBuilder
private var generateButton: some View { private var generateButton: some View {
if let url = container.mediaAttachment?.url, preferences.isOpenAIEnabled { if let url = container.mediaAttachment?.url, preferences.isOpenAIEnabled {
@ -133,7 +133,7 @@ struct StatusEditorMediaEditView: View {
} }
} }
} }
@ViewBuilder @ViewBuilder
private var translateButton: some View { private var translateButton: some View {
if showTranslateButton { if showTranslateButton {
@ -153,10 +153,9 @@ struct StatusEditorMediaEditView: View {
Text("status.action.translate") Text("status.action.translate")
} }
} }
} }
} }
private func generateDescription(url: URL) async -> String? { private func generateDescription(url: URL) async -> String? {
isGeneratingDescription = true isGeneratingDescription = true
let client = OpenAIClient() let client = OpenAIClient()
@ -164,7 +163,7 @@ struct StatusEditorMediaEditView: View {
isGeneratingDescription = false isGeneratingDescription = false
return response?.trimmedText return response?.trimmedText
} }
private func translateDescription() async -> String? { private func translateDescription() async -> String? {
isTranslating = true isTranslating = true
let userAPIKey = DeepLUserAPIHandler.readIfAllowed() let userAPIKey = DeepLUserAPIHandler.readIfAllowed()

View file

@ -1,10 +1,10 @@
import AVKit import AVKit
import DesignSystem import DesignSystem
import Env import Env
import MediaUI
import Models import Models
import NukeUI import NukeUI
import SwiftUI import SwiftUI
import MediaUI
@MainActor @MainActor
struct StatusEditorMediaView: View { struct StatusEditorMediaView: View {
@ -49,17 +49,17 @@ struct StatusEditorMediaView: View {
private let containerHeight: CGFloat = 300 private let containerHeight: CGFloat = 300
private var containerWidth: CGFloat { containerHeight / 1.5 } private var containerWidth: CGFloat { containerHeight / 1.5 }
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
private var showsScrollIndicators : Bool { count > 1 } private var showsScrollIndicators: Bool { count > 1 }
private var scrollBottomPadding : CGFloat? = nil private var scrollBottomPadding: CGFloat?
#else #else
private var showsScrollIndicators : Bool = false private var showsScrollIndicators: Bool = false
private var scrollBottomPadding : CGFloat? = 0 private var scrollBottomPadding: CGFloat? = 0
#endif #endif
init(viewModel: StatusEditorViewModel, editingMediaContainer: Binding<StatusEditorMediaContainer?>) { init(viewModel: StatusEditorViewModel, editingMediaContainer: Binding<StatusEditorMediaContainer?>) {
self.viewModel = viewModel self.viewModel = viewModel
self._editingMediaContainer = editingMediaContainer _editingMediaContainer = editingMediaContainer
} }
private func pixel(at index: Int) -> some View { private func pixel(at index: Int) -> some View {

View file

@ -1,10 +1,10 @@
import SwiftUI
import Models
import Env
import DesignSystem
import Accounts import Accounts
import AppAccount import AppAccount
import DesignSystem
import Env
import Models
import Network import Network
import SwiftUI
@MainActor @MainActor
struct StatusEditorCoreView: View { struct StatusEditorCoreView: View {
@ -22,11 +22,11 @@ struct StatusEditorCoreView: View {
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount
@Environment(AppAccountsManager.self) private var appAccounts @Environment(AppAccountsManager.self) private var appAccounts
@Environment(Client.self) private var client @Environment(Client.self) private var client
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
@Environment(\.dismissWindow) private var dismissWindow @Environment(\.dismissWindow) private var dismissWindow
#else #else
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
#endif #endif
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
@ -145,11 +145,11 @@ struct StatusEditorCoreView: View {
viewModel.preferences = preferences viewModel.preferences = preferences
viewModel.prepareStatusText() viewModel.prepareStatusText()
if !client.isAuth { if !client.isAuth {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
dismissWindow() dismissWindow()
#else #else
dismiss() dismiss()
#endif #endif
NotificationCenter.default.post(name: .shareSheetClose, object: nil) NotificationCenter.default.post(name: .shareSheetClose, object: nil)
} }

View file

@ -1,5 +1,5 @@
import SwiftUI
import Models import Models
import SwiftUI
struct StatusEditorPrivacyMenu: View { struct StatusEditorPrivacyMenu: View {
@Binding var visibility: Models.Visibility @Binding var visibility: Models.Visibility
@ -8,7 +8,7 @@ struct StatusEditorPrivacyMenu: View {
var body: some View { var body: some View {
Menu { Menu {
ForEach(Models.Visibility.allCases, id: \.self) { vis in ForEach(Models.Visibility.allCases, id: \.self) { vis in
Button { self.visibility = vis } label: { Button { visibility = vis } label: {
Label(vis.title, systemImage: vis.iconName) Label(vis.title, systemImage: vis.iconName)
} }
} }
@ -29,4 +29,3 @@ struct StatusEditorPrivacyMenu: View {
} }
} }
} }

View file

@ -1,7 +1,7 @@
import SwiftUI
import Env import Env
import Models import Models
import StoreKit import StoreKit
import SwiftUI
@MainActor @MainActor
struct StatusEditorToolbarItems: ToolbarContent { struct StatusEditorToolbarItems: ToolbarContent {
@ -12,11 +12,11 @@ struct StatusEditorToolbarItems: ToolbarContent {
@Environment(\.modelContext) private var context @Environment(\.modelContext) private var context
@Environment(UserPreferences.self) private var preferences @Environment(UserPreferences.self) private var preferences
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
@Environment(\.dismissWindow) private var dismissWindow @Environment(\.dismissWindow) private var dismissWindow
#else #else
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
#endif #endif
var body: some ToolbarContent { var body: some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
@ -81,18 +81,18 @@ struct StatusEditorToolbarItems: ToolbarContent {
private func postStatus(with model: StatusEditorViewModel, isMainPost: Bool) async -> Status? { private func postStatus(with model: StatusEditorViewModel, isMainPost: Bool) async -> Status? {
let status = await model.postStatus() let status = await model.postStatus()
if status != nil && isMainPost { if status != nil, isMainPost {
close() close()
SoundEffectManager.shared.playSound(.tootSent) SoundEffectManager.shared.playSound(.tootSent)
NotificationCenter.default.post(name: .shareSheetClose, object: nil) NotificationCenter.default.post(name: .shareSheetClose, object: nil)
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
if !mainSEVM.mode.isInShareExtension, !preferences.requestedReview { if !mainSEVM.mode.isInShareExtension, !preferences.requestedReview {
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene) SKStoreReviewController.requestReview(in: scene)
}
preferences.requestedReview = true
} }
preferences.requestedReview = true #endif
}
#endif
} }
return status return status
@ -109,11 +109,11 @@ struct StatusEditorToolbarItems: ToolbarContent {
} }
} }
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
private func close() { dismissWindow() } private func close() { dismissWindow() }
#else #else
private func close() { dismiss() } private func close() { dismiss() }
#endif #endif
@ViewBuilder @ViewBuilder
private var languageConfirmationDialog: some View { private var languageConfirmationDialog: some View {

View file

@ -24,7 +24,7 @@ public struct StatusEditorView: View {
@State private var scrollID: UUID? @State private var scrollID: UUID?
@FocusState private var editorFocusState: StatusEditorFocusState? @FocusState private var editorFocusState: StatusEditorFocusState?
private var focusedSEVM: StatusEditorViewModel { private var focusedSEVM: StatusEditorViewModel {
if case let .followUp(id) = editorFocusState, if case let .followUp(id) = editorFocusState,
let sevm = followUpSEVMs.first(where: { $0.id == id }) let sevm = followUpSEVMs.first(where: { $0.id == id })
@ -38,7 +38,7 @@ public struct StatusEditorView: View {
} }
public var body: some View { public var body: some View {
@Bindable var focusedSEVM = self.focusedSEVM @Bindable var focusedSEVM = focusedSEVM
NavigationStack { NavigationStack {
ScrollView { ScrollView {
@ -54,7 +54,7 @@ public struct StatusEditorView: View {
) )
.id(mainSEVM.id) .id(mainSEVM.id)
ForEach(followUpSEVMs) { sevm in ForEach(followUpSEVMs) { sevm in
@Bindable var sevm: StatusEditorViewModel = sevm @Bindable var sevm: StatusEditorViewModel = sevm
StatusEditorCoreView( StatusEditorCoreView(
@ -93,7 +93,8 @@ public struct StatusEditorView: View {
Button("OK") {} Button("OK") {}
}, message: { }, message: {
Text(mainSEVM.postingError ?? "") Text(mainSEVM.postingError ?? "")
}) }
)
.interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning) .interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning)
.onChange(of: appAccounts.currentClient) { _, newValue in .onChange(of: appAccounts.currentClient) { _, newValue in
if mainSEVM.mode.isInShareExtension { if mainSEVM.mode.isInShareExtension {

View file

@ -10,7 +10,7 @@ import SwiftUI
@MainActor @MainActor
@Observable public class StatusEditorViewModel: NSObject, Identifiable { @Observable public class StatusEditorViewModel: NSObject, Identifiable {
public let id = UUID() public let id = UUID()
var mode: Mode var mode: Mode
var client: Client? var client: Client?
@ -94,7 +94,7 @@ import SwiftUI
let removedIDs = oldValue let removedIDs = oldValue
.filter { !mediaPickers.contains($0) } .filter { !mediaPickers.contains($0) }
.compactMap { $0.itemIdentifier } .compactMap(\.itemIdentifier)
mediaContainers.removeAll { removedIDs.contains($0.id) } mediaContainers.removeAll { removedIDs.contains($0.id) }
let newPickerItems = mediaPickers.filter { !oldValue.contains($0) } let newPickerItems = mediaPickers.filter { !oldValue.contains($0) }
@ -297,7 +297,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: $0, mediaAttachment: $0,
error: nil) error: nil
)
} }
case let .quote(status): case let .quote(status):
embeddedStatus = status embeddedStatus = status
@ -384,7 +385,7 @@ import SwiftUI
.compactMap { NSItemProvider(contentsOf: $0) } .compactMap { NSItemProvider(contentsOf: $0) }
processItemsProvider(items: items) processItemsProvider(items: items)
} }
func processGIFData(data: Data) { func processGIFData(data: Data) {
isMediasLoading = true isMediasLoading = true
let url = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).gif") let url = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).gif")
@ -405,7 +406,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
prepareToPost(for: container) prepareToPost(for: container)
} }
@ -428,7 +430,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
prepareToPost(for: container) prepareToPost(for: container)
} else if let content = content as? ImageFileTranseferable, } else if let content = content as? ImageFileTranseferable,
let compressedData = await compressor.compressImageFrom(url: content.url), let compressedData = await compressor.compressImageFrom(url: content.url),
@ -440,7 +443,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
prepareToPost(for: container) prepareToPost(for: container)
} else if let video = content as? MovieFileTranseferable { } else if let video = content as? MovieFileTranseferable {
let container = StatusEditorMediaContainer( let container = StatusEditorMediaContainer(
@ -449,7 +453,8 @@ import SwiftUI
movieTransferable: video, movieTransferable: video,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
prepareToPost(for: container) prepareToPost(for: container)
} else if let gif = content as? GifFileTranseferable { } else if let gif = content as? GifFileTranseferable {
let container = StatusEditorMediaContainer( let container = StatusEditorMediaContainer(
@ -458,7 +463,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: gif, gifTransferable: gif,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
prepareToPost(for: container) prepareToPost(for: container)
} }
} catch { } catch {
@ -619,7 +625,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: gifFile, gifTransferable: gifFile,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
} }
private static func makeMovieContainer(from pickerItem: PhotosPickerItem) async -> StatusEditorMediaContainer? { private static func makeMovieContainer(from pickerItem: PhotosPickerItem) async -> StatusEditorMediaContainer? {
@ -631,7 +638,8 @@ import SwiftUI
movieTransferable: movieFile, movieTransferable: movieFile,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
} }
private static func makeImageContainer(from pickerItem: PhotosPickerItem) async -> StatusEditorMediaContainer? { private static func makeImageContainer(from pickerItem: PhotosPickerItem) async -> StatusEditorMediaContainer? {
@ -649,7 +657,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
} }
func upload(container: StatusEditorMediaContainer) async { func upload(container: StatusEditorMediaContainer) async {
@ -662,7 +671,8 @@ import SwiftUI
movieTransferable: originalContainer.movieTransferable, movieTransferable: originalContainer.movieTransferable,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: nil) error: nil
)
mediaContainers[index] = newContainer mediaContainers[index] = newContainer
do { do {
let compressor = StatusEditorCompressor() let compressor = StatusEditorCompressor()
@ -676,7 +686,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: uploadedMedia, mediaAttachment: uploadedMedia,
error: nil) error: nil
)
} }
if let uploadedMedia, uploadedMedia.url == nil { if let uploadedMedia, uploadedMedia.url == nil {
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia) scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
@ -693,7 +704,8 @@ import SwiftUI
movieTransferable: originalContainer.movieTransferable, movieTransferable: originalContainer.movieTransferable,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: uploadedMedia, mediaAttachment: uploadedMedia,
error: nil) error: nil
)
} }
if let uploadedMedia, uploadedMedia.url == nil { if let uploadedMedia, uploadedMedia.url == nil {
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia) scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
@ -707,7 +719,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: originalContainer.gifTransferable, gifTransferable: originalContainer.gifTransferable,
mediaAttachment: uploadedMedia, mediaAttachment: uploadedMedia,
error: nil) error: nil
)
} }
if let uploadedMedia, uploadedMedia.url == nil { if let uploadedMedia, uploadedMedia.url == nil {
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia) scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
@ -721,7 +734,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: nil, mediaAttachment: nil,
error: error) error: error
)
} }
} }
} }
@ -747,7 +761,8 @@ import SwiftUI
movieTransferable: oldContainer.movieTransferable, movieTransferable: oldContainer.movieTransferable,
gifTransferable: oldContainer.gifTransferable, gifTransferable: oldContainer.gifTransferable,
mediaAttachment: newAttachement, mediaAttachment: newAttachement,
error: nil) error: nil
)
} }
} catch { } catch {
print(error.localizedDescription) print(error.localizedDescription)
@ -770,7 +785,8 @@ import SwiftUI
movieTransferable: nil, movieTransferable: nil,
gifTransferable: nil, gifTransferable: nil,
mediaAttachment: media, mediaAttachment: media,
error: nil) error: nil
)
} catch { print(error) } } catch { print(error) }
} }
} }
@ -815,9 +831,9 @@ import SwiftUI
return dict return dict
}.sorted(by: { lhs, rhs in }.sorted(by: { lhs, rhs in
if rhs.key == "Uncategorized" { return false } if rhs.key == "Uncategorized" { false }
else if lhs.key == "Uncategorized" { return true } else if lhs.key == "Uncategorized" { true }
else { return lhs.key < rhs.key } else { lhs.key < rhs.key }
}).forEach { key, value in }).forEach { key, value in
emojiContainers.append(.init(categoryName: key, emojis: value)) emojiContainers.append(.init(categoryName: key, emojis: value))
} }

View file

@ -45,10 +45,9 @@ extension TextView.Representable {
textView.allowsEditingTextAttributes = false textView.allowsEditingTextAttributes = false
textView.returnKeyType = .default textView.returnKeyType = .default
textView.allowsEditingTextAttributes = true textView.allowsEditingTextAttributes = true
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
textView.inlinePredictionType = .no textView.inlinePredictionType = .no
#endif #endif
self.getTextView?(textView) self.getTextView?(textView)
} }

View file

@ -36,7 +36,7 @@ public struct StatusRowView: View {
HStack(spacing: 0) { HStack(spacing: 0) {
if !isCompact { if !isCompact {
HStack(spacing: 3) { HStack(spacing: 3) {
ForEach(0..<indentationLevel, id: \.self) { level in ForEach(0 ..< indentationLevel, id: \.self) { level in
Rectangle() Rectangle()
.fill(theme.tintColor) .fill(theme.tintColor)
.frame(width: 2) .frame(width: 2)
@ -225,13 +225,14 @@ public struct StatusRowView: View {
Button("accessibility.status.media-viewer-action.label") { Button("accessibility.status.media-viewer-action.label") {
HapticManager.shared.fireHaptic(.notification(.success)) HapticManager.shared.fireHaptic(.notification(.success))
let attachments = viewModel.finalStatus.mediaAttachments let attachments = viewModel.finalStatus.mediaAttachments
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationMedia.mediaViewer( openWindow(value: WindowDestinationMedia.mediaViewer(
attachments: attachments, attachments: attachments,
selectedAttachment: attachments[0])) selectedAttachment: attachments[0]
#else ))
quickLook.prepareFor(selectedMediaAttachment: attachments[0], mediaAttachments: attachments) #else
#endif quickLook.prepareFor(selectedMediaAttachment: attachments[0], mediaAttachments: attachments)
#endif
} }
} }

View file

@ -39,7 +39,7 @@ import SwiftUI
var isLoadingRemoteContent: Bool = false var isLoadingRemoteContent: Bool = false
var localStatusId: String? var localStatusId: String?
var localStatus: Status? var localStatus: Status?
private var scrollToId = nil as Binding<String?>? private var scrollToId = nil as Binding<String?>?
// The relationship our user has to the author of this post, if available // The relationship our user has to the author of this post, if available
@ -199,9 +199,9 @@ import SwiftUI
routerPath.navigate(to: .accountDetail(id: mention.id)) routerPath.navigate(to: .accountDetail(id: mention.id))
} }
} }
func goToParent() { func goToParent() {
guard let id = status.inReplyToId else {return} guard let id = status.inReplyToId else { return }
if let _ = scrollToId { if let _ = scrollToId {
scrollToId?.wrappedValue = id scrollToId?.wrappedValue = id
} else { } else {

View file

@ -205,11 +205,11 @@ struct StatusRowActionsView: View {
switch action { switch action {
case .respond: case .respond:
SoundEffectManager.shared.playSound(.share) SoundEffectManager.shared.playSound(.share)
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status)) openWindow(value: WindowDestinationEditor.replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status))
#else #else
viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status) viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.localStatus ?? viewModel.status)
#endif #endif
case .favorite: case .favorite:
SoundEffectManager.shared.playSound(.favorite) SoundEffectManager.shared.playSound(.favorite)
await statusDataController.toggleFavorite(remoteStatus: viewModel.localStatusId) await statusDataController.toggleFavorite(remoteStatus: viewModel.localStatusId)

View file

@ -5,6 +5,7 @@ import NukeUI
import Shimmer import Shimmer
import SwiftUI import SwiftUI
@MainActor
public struct StatusRowCardView: View { public struct StatusRowCardView: View {
@Environment(\.openURL) private var openURL @Environment(\.openURL) private var openURL
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@ -46,7 +47,7 @@ public struct StatusRowCardView: View {
} label: { } label: {
if let title = card.title, let url = URL(string: card.url) { if let title = card.title, let url = URL(string: card.url) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
let sitesWithIcons = [ "apps.apple.com", "music.apple.com", "open.spotify.com" ] let sitesWithIcons = ["apps.apple.com", "music.apple.com", "open.spotify.com"]
if let host = url.host(), sitesWithIcons.contains(host) { if let host = url.host(), sitesWithIcons.contains(host) {
iconLinkPreview(title, url) iconLinkPreview(title, url)
} else { } else {
@ -83,56 +84,53 @@ public struct StatusRowCardView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@MainActor @ViewBuilder
private func defaultLinkPreview(_ title: String, _ url: URL) -> some View { private func defaultLinkPreview(_ title: String, _ url: URL) -> some View {
Group { if let imageURL = card.image, !isInCaptureMode {
if let imageURL = card.image, !isInCaptureMode { LazyResizableImage(url: imageURL) { state, proxy in
LazyResizableImage(url: imageURL) { state, proxy in let width = imageWidthFor(proxy: proxy)
let width = imageWidthFor(proxy: proxy) if let image = state.image {
if let image = state.image { image
image .resizable()
.resizable() .aspectRatio(contentMode: .fill)
.aspectRatio(contentMode: .fill) .frame(height: imageHeight)
.frame(height: imageHeight) .frame(maxWidth: width)
.frame(maxWidth: width) .clipped()
.clipped() } else if state.isLoading {
} else if state.isLoading { Rectangle()
Rectangle() .fill(Color.gray)
.fill(Color.gray) .frame(height: imageHeight)
.frame(height: imageHeight)
}
} }
// This image is decorative
.accessibilityHidden(true)
.frame(height: imageHeight)
} }
HStack { // This image is decorative
VStack(alignment: .leading, spacing: 6) { .accessibilityHidden(true)
Text(title) .frame(height: imageHeight)
.font(.scaledHeadline)
.lineLimit(3)
if let description = card.description, !description.isEmpty {
Text(description)
.font(.scaledBody)
.foregroundStyle(.secondary)
.lineLimit(3)
}
Text(url.host() ?? url.absoluteString)
.font(.scaledFootnote)
.foregroundColor(theme.tintColor)
.lineLimit(1)
}
Spacer()
}.padding(16)
} }
HStack {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.scaledHeadline)
.lineLimit(3)
if let description = card.description, !description.isEmpty {
Text(description)
.font(.scaledBody)
.foregroundStyle(.secondary)
.lineLimit(3)
}
Text(url.host() ?? url.absoluteString)
.font(.scaledFootnote)
.foregroundColor(theme.tintColor)
.lineLimit(1)
}
Spacer()
}.padding(16)
} }
@MainActor
private func iconLinkPreview(_ title: String, _ url: URL) -> some View { private func iconLinkPreview(_ title: String, _ url: URL) -> some View {
// ..where the image is known to be a square icon // ..where the image is known to be a square icon
HStack { HStack {
if let imageURL = card.image, !isInCaptureMode { if let imageURL = card.image, !isInCaptureMode {
LazyResizableImage(url: imageURL) { state, proxy in LazyResizableImage(url: imageURL) { state, _ in
if let image = state.image { if let image = state.image {
image image
.resizable() .resizable()
@ -149,7 +147,7 @@ public struct StatusRowCardView: View {
.accessibilityHidden(true) .accessibilityHidden(true)
.frame(width: imageHeight, height: imageHeight) .frame(width: imageHeight, height: imageHeight)
} }
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(title) Text(title)
.font(.scaledHeadline) .font(.scaledHeadline)

View file

@ -55,20 +55,20 @@ struct StatusRowContextMenu: View {
systemImage: "bookmark") systemImage: "bookmark")
} }
Button { Button {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.replyToStatusEditor(status: viewModel.status)) openWindow(value: WindowDestinationEditor.replyToStatusEditor(status: viewModel.status))
#else #else
viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status) viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status)
#endif #endif
} label: { } label: {
Label("status.action.reply", systemImage: "arrowshape.turn.up.left") Label("status.action.reply", systemImage: "arrowshape.turn.up.left")
} }
Button { Button {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.quoteStatusEditor(status: viewModel.status)) openWindow(value: WindowDestinationEditor.quoteStatusEditor(status: viewModel.status))
#else #else
viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status) viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
#endif #endif
} label: { } label: {
Label("status.action.quote", systemImage: "quote.bubble") Label("status.action.quote", systemImage: "quote.bubble")
} }
@ -171,11 +171,11 @@ struct StatusRowContextMenu: View {
} }
if currentInstance.isEditSupported { if currentInstance.isEditSupported {
Button { Button {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow(value: WindowDestinationEditor.editStatusEditor(status: viewModel.status.reblogAsAsStatus ?? viewModel.status)) openWindow(value: WindowDestinationEditor.editStatusEditor(status: viewModel.status.reblogAsAsStatus ?? viewModel.status))
#else #else
viewModel.routerPath.presentedSheet = .editStatusEditor(status: viewModel.status.reblogAsAsStatus ?? viewModel.status) viewModel.routerPath.presentedSheet = .editStatusEditor(status: viewModel.status.reblogAsAsStatus ?? viewModel.status)
#endif #endif
} label: { } label: {
Label("status.action.edit", systemImage: "pencil") Label("status.action.edit", systemImage: "pencil")
} }
@ -307,7 +307,7 @@ struct SelectTextView: View {
struct SelectableText: UIViewRepresentable { struct SelectableText: UIViewRepresentable {
let content: AttributedString let content: AttributedString
func makeUIView(context: Context) -> UITextView { func makeUIView(context _: Context) -> UITextView {
let attributedText = NSMutableAttributedString(content) let attributedText = NSMutableAttributedString(content)
attributedText.addAttribute( attributedText.addAttribute(
.font, .font,
@ -323,6 +323,6 @@ struct SelectableText: UIViewRepresentable {
return textView return textView
} }
func updateUIView(_ uiView: UITextView, context: Context) {} func updateUIView(_: UITextView, context _: Context) {}
func makeCoordinator() -> Void {} func makeCoordinator() {}
} }

View file

@ -55,13 +55,13 @@ struct StatusRowHeaderView: View {
Group { Group {
EmojiTextApp(.init(stringValue: viewModel.finalStatus.account.safeDisplayName), EmojiTextApp(.init(stringValue: viewModel.finalStatus.account.safeDisplayName),
emojis: viewModel.finalStatus.account.emojis) emojis: viewModel.finalStatus.account.emojis)
.font(.scaledSubheadline) .font(.scaledSubheadline)
.foregroundColor(theme.labelColor) .foregroundColor(theme.labelColor)
.emojiSize(Font.scaledSubheadlineFont.emojiSize) .emojiSize(Font.scaledSubheadlineFont.emojiSize)
.emojiBaselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset) .emojiBaselineOffset(Font.scaledSubheadlineFont.emojiBaselineOffset)
.fontWeight(.semibold) .fontWeight(.semibold)
.lineLimit(1) .lineLimit(1)
.accountPopover(viewModel.finalStatus.account) .accountPopover(viewModel.finalStatus.account)
accountBadgeView accountBadgeView
.font(.footnote) .font(.footnote)

View file

@ -102,19 +102,19 @@ public struct StatusRowMediaPreviewView: View {
} }
private func tabAction(for index: Int) { private func tabAction(for index: Int) {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
openWindow( openWindow(
value: WindowDestinationMedia.mediaViewer( value: WindowDestinationMedia.mediaViewer(
attachments: attachments, attachments: attachments,
selectedAttachment: attachments[index] selectedAttachment: attachments[index]
) )
) )
#else #else
quickLook.prepareFor( quickLook.prepareFor(
selectedMediaAttachment: attachments[index], selectedMediaAttachment: attachments[index],
mediaAttachments: attachments mediaAttachments: attachments
) )
#endif #endif
} }
private static func accessibilityLabel(for attachment: MediaAttachment) -> Text { private static func accessibilityLabel(for attachment: MediaAttachment) -> Text {
@ -137,10 +137,10 @@ private struct MediaPreview: View {
@Environment(\.isCompact) private var isCompact: Bool @Environment(\.isCompact) private var isCompact: Bool
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { _ in
switch displayData.type { switch displayData.type {
case .image: case .image:
LazyResizableImage(url: displayData.previewUrl) { state, proxy in LazyResizableImage(url: displayData.previewUrl) { state, _ in
if let image = state.image { if let image = state.image {
image image
.resizable() .resizable()
@ -247,7 +247,7 @@ private struct FeaturedImagePreView: View {
let boxWidth = availableWidth - appLayoutWidth let boxWidth = availableWidth - appLayoutWidth
let boxHeight = availableHeight * 0.8 // use only 80% of window height to leave room for text let boxHeight = availableHeight * 0.8 // use only 80% of window height to leave room for text
if from.width <= boxWidth && from.height <= boxHeight { if from.width <= boxWidth, from.height <= boxHeight {
// intrinsic size of image fits just fine // intrinsic size of image fits just fine
return from return from
} }

View file

@ -7,30 +7,30 @@ struct StatusRowReplyView: View {
var body: some View { var body: some View {
Group { Group {
if let accountId = viewModel.status.inReplyToAccountId { if let accountId = viewModel.status.inReplyToAccountId {
Group { Group {
if let mention = viewModel.status.mentions.first(where: { $0.id == accountId }) { if let mention = viewModel.status.mentions.first(where: { $0.id == accountId }) {
HStack(spacing: 2) { HStack(spacing: 2) {
Image(systemName: "arrowshape.turn.up.left.fill") Image(systemName: "arrowshape.turn.up.left.fill")
Text("status.row.was-reply \(mention.username)") Text("status.row.was-reply \(mention.username)")
} }
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
.accessibilityLabel( .accessibilityLabel(
Text("status.row.was-reply \(mention.username)") Text("status.row.was-reply \(mention.username)")
) )
} else if viewModel.isThread && accountId == viewModel.status.account.id { } else if viewModel.isThread, accountId == viewModel.status.account.id {
HStack(spacing: 2) { HStack(spacing: 2) {
Image(systemName: "quote.opening") Image(systemName: "quote.opening")
Text("status.row.is-thread") Text("status.row.is-thread")
} }
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
.accessibilityLabel( .accessibilityLabel(
Text("status.row.is-thread") Text("status.row.is-thread")
) )
}
}
.onTapGesture {
viewModel.goToParent()
} }
}
.onTapGesture {
viewModel.goToParent()
}
} }
} }
.font(.scaledFootnote) .font(.scaledFootnote)

View file

@ -1,6 +1,6 @@
import DesignSystem import DesignSystem
import SwiftUI
import Env import Env
import SwiftUI
struct StatusRowTagView: View { struct StatusRowTagView: View {
@Environment(CurrentAccount.self) private var currentAccount @Environment(CurrentAccount.self) private var currentAccount

View file

@ -1,3 +1,4 @@
import Charts
import DesignSystem import DesignSystem
import Env import Env
import Models import Models
@ -7,7 +8,6 @@ import Status
import SwiftData import SwiftData
import SwiftUI import SwiftUI
import SwiftUIIntrospect import SwiftUIIntrospect
import Charts
@MainActor @MainActor
public struct TimelineView: View { public struct TimelineView: View {
@ -157,7 +157,7 @@ public struct TimelineView: View {
HStack { HStack {
TagChartView(tag: tag) TagChartView(tag: tag)
.padding(.top, 12) .padding(.top, 12)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("#\(tag.name)") Text("#\(tag.name)")
.font(.scaledHeadline) .font(.scaledHeadline)

View file

@ -202,7 +202,7 @@ extension TimelineViewModel: StatusesFetcher {
// Else we fetch top most page from the API. // Else we fetch top most page from the API.
if let cachedStatuses = await getCachedStatuses(), if let cachedStatuses = await getCachedStatuses(),
!cachedStatuses.isEmpty, !cachedStatuses.isEmpty,
timeline == .home && !UserPreferences.shared.fastRefreshEnabled timeline == .home, !UserPreferences.shared.fastRefreshEnabled
{ {
await datasource.set(cachedStatuses) await datasource.set(cachedStatuses)
if let latestSeenId = await cache.getLatestSeenStatus(for: client)?.last, if let latestSeenId = await cache.getLatestSeenStatus(for: client)?.last,