This commit is contained in:
Thomas Ricouard 2024-05-04 13:19:19 +02:00
parent ba4cc899f8
commit c3edabb183
25 changed files with 269 additions and 276 deletions

View file

@ -8,10 +8,10 @@ import LinkPresentation
import Lists
import MediaUI
import Models
import Notifications
import StatusKit
import SwiftUI
import Timeline
import Notifications
@MainActor
extension View {
@ -67,7 +67,7 @@ extension View {
case .notificationsRequests:
NotificationsRequestsListView()
case let .notificationForAccount(accountId):
NotificationsListView(lockedType: nil ,
NotificationsListView(lockedType: nil,
lockedAccountId: accountId,
scrollToTopSignal: .constant(0))
case .blockedAccounts:

View file

@ -1,8 +1,8 @@
import AppIntents
import Env
import MediaUI
import StatusKit
import SwiftUI
import AppIntents
extension IceCubesApp {
var appScene: some Scene {
@ -126,19 +126,20 @@ extension IceCubesApp {
.windowResizability(.contentMinSize)
}
private func handleIntent(_ intent: any AppIntent) {
private func handleIntent(_: any AppIntent) {
if let postIntent = appIntentService.handledIntent?.intent as? PostIntent {
#if os(visionOS) || os(macOS)
openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility))
openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility))
#else
appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility)
appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "",
visibility: userPreferences.postVisibility)
#endif
} else if let tabIntent = appIntentService.handledIntent?.intent as? TabIntent {
selectedTab = tabIntent.tab.toAppTab
} else if let imageIntent = appIntentService.handledIntent?.intent as? PostImageIntent,
let urls = imageIntent.images?.compactMap({ $0.fileURL }) {
let urls = imageIntent.images?.compactMap({ $0.fileURL })
{
appRouterPath.presentedSheet = .imageURL(urls: urls,
visibility: userPreferences.postVisibility)
}

View file

@ -1,10 +1,10 @@
import Account
import AppIntents
import DesignSystem
import Explore
import Foundation
import StatusKit
import SwiftUI
import AppIntents
@MainActor
enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {

View file

@ -1,5 +1,5 @@
import SwiftUI
import AppIntents
import SwiftUI
@Observable
public class AppIntentService: @unchecked Sendable {
@ -12,7 +12,7 @@ public class AppIntentService: @unchecked Sendable {
let intent: any AppIntent
init(intent: any AppIntent) {
self.id = UUID().uuidString
id = UUID().uuidString
self.intent = intent
}
}
@ -21,5 +21,5 @@ public class AppIntentService: @unchecked Sendable {
var handledIntent: HandledIntent?
private init() { }
private init() {}
}

View file

@ -1,36 +1,34 @@
import Foundation
import AppIntents
import AppAccount
import Network
import AppIntents
import Env
import Foundation
import Models
import Network
enum PostVisibility: String, AppEnum {
case direct, priv, unlisted, pub
public static var caseDisplayRepresentations: [PostVisibility : DisplayRepresentation] {
public static var caseDisplayRepresentations: [PostVisibility: DisplayRepresentation] {
[.direct: "Private",
.priv: "Followers Only",
.unlisted: "Quiet Public",
.pub: "Public"]
.priv: "Followers Only",
.unlisted: "Quiet Public",
.pub: "Public"]
}
static var typeDisplayName: LocalizedStringResource {
get { "Visibility" }
}
static var typeDisplayName: LocalizedStringResource { "Visibility" }
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Visibility"
var toAppVisibility: Models.Visibility {
switch self {
case .direct:
.direct
.direct
case .priv:
.priv
.priv
case .unlisted:
.unlisted
.unlisted
case .pub:
.pub
.pub
}
}
}
@ -47,21 +45,19 @@ struct AppAccountWrapper: Identifiable, AppEntity {
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(account.accountName ?? account.server)")
}
}
struct DefaultAppAccountQuery: EntityQuery {
func entities(for identifiers: [AppAccountWrapper.ID]) async throws -> [AppAccountWrapper] {
return await AppAccountsManager.shared.availableAccounts.filter { account in
identifiers.contains { id in
id == account.id
}
}.map{ AppAccountWrapper(account: $0 )}
}.map { AppAccountWrapper(account: $0) }
}
func suggestedEntities() async throws -> [AppAccountWrapper] {
await AppAccountsManager.shared.availableAccounts.map{ .init(account: $0)}
await AppAccountsManager.shared.availableAccounts.map { .init(account: $0) }
}
func defaultResult() async -> AppAccountWrapper? {
@ -72,10 +68,9 @@ struct DefaultAppAccountQuery: EntityQuery {
struct InlinePostIntent: AppIntent {
static let title: LocalizedStringResource = "Send text status to Mastodon"
static var description: IntentDescription {
get {
"Send a text status to Mastodon using Ice Cubes"
}
"Send a text status to Mastodon using Ice Cubes"
}
static let openAppWhenRun: Bool = false
@Parameter(title: "Account", requestValueDialog: IntentDialog("Account"))

View file

@ -1,13 +1,12 @@
import Foundation
import AppIntents
import Foundation
struct PostImageIntent: AppIntent {
static let title: LocalizedStringResource = "Post an image to Mastodon"
static var description: IntentDescription {
get {
"Use Ice Cubes to post a status with an image to Mastodon"
}
"Use Ice Cubes to post a status with an image to Mastodon"
}
static let openAppWhenRun: Bool = true
@Parameter(title: "Image",

View file

@ -1,13 +1,12 @@
import Foundation
import AppIntents
import Foundation
struct PostIntent: AppIntent {
static let title: LocalizedStringResource = "Post status to Mastodon"
static var description: IntentDescription {
get {
"Use Ice Cubes to post a status to Mastodon"
}
"Use Ice Cubes to post a status to Mastodon"
}
static let openAppWhenRun: Bool = true
@Parameter(title: "Post content", inputConnectionBehavior: .connectToPreviousIntentResult)

View file

@ -1,5 +1,5 @@
import Foundation
import AppIntents
import Foundation
enum TabEnum: String, AppEnum, Sendable {
case timeline, notifications, mentions, explore, messages, settings
@ -12,13 +12,11 @@ enum TabEnum: String, AppEnum, Sendable {
case lists
case links
static var typeDisplayName: LocalizedStringResource {
get { "Tab" }
}
static var typeDisplayName: LocalizedStringResource { "Tab" }
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Tab"
nonisolated static var caseDisplayRepresentations: [TabEnum : DisplayRepresentation] {
nonisolated static var caseDisplayRepresentations: [TabEnum: DisplayRepresentation] {
[.timeline: .init(title: "Home Timeline"),
.trending: .init(title: "Trending Timeline"),
.federated: .init(title: "Federated Timeline"),
@ -34,44 +32,43 @@ enum TabEnum: String, AppEnum, Sendable {
.followedTags: .init(title: "Followed Tags"),
.lists: .init(title: "Lists"),
.links: .init(title: "Trending Links"),
.post: .init(title: "New post"),
]
.post: .init(title: "New post")]
}
var toAppTab: Tab {
switch self {
case .timeline:
.timeline
.timeline
case .notifications:
.notifications
.notifications
case .mentions:
.mentions
.mentions
case .explore:
.explore
.explore
case .messages:
.messages
.messages
case .settings:
.settings
.settings
case .trending:
.trending
.trending
case .federated:
.federated
.federated
case .local:
.local
.local
case .profile:
.profile
.profile
case .bookmarks:
.bookmarks
.bookmarks
case .favorites:
.favorites
.favorites
case .post:
.post
.post
case .followedTags:
.followedTags
.followedTags
case .lists:
.lists
.lists
case .links:
.links
.links
}
}
}
@ -79,10 +76,9 @@ enum TabEnum: String, AppEnum, Sendable {
struct TabIntent: AppIntent {
static let title: LocalizedStringResource = "Open on a tab"
static var description: IntentDescription {
get {
"Open the app on a specific tab"
}
"Open the app on a specific tab"
}
static let openAppWhenRun: Bool = true
@Parameter(title: "Selected tab")

View file

@ -333,7 +333,6 @@ public struct AccountDetailView: View {
Label("account.blocked", systemImage: "person.crop.circle.badge.xmark")
}
Button {
routerPath.navigate(to: .mutedAccounts)
} label: {

View file

@ -10,7 +10,7 @@ public struct CloseToolbarItem: ToolbarContent {
Button(action: {
dismiss()
}, label: {
Image(systemName: "xmark.circle")
Image(systemName: "xmark.circle")
})
.keyboardShortcut(.cancelAction)
}

View file

@ -4,9 +4,9 @@ public struct ErrorView: View {
public let title: LocalizedStringKey
public let message: LocalizedStringKey
public let buttonTitle: LocalizedStringKey
public let onButtonPress: (() async -> Void)
public let onButtonPress: () async -> Void
public init(title: LocalizedStringKey, message: LocalizedStringKey, buttonTitle: LocalizedStringKey, onButtonPress: @escaping (() async -> Void) ) {
public init(title: LocalizedStringKey, message: LocalizedStringKey, buttonTitle: LocalizedStringKey, onButtonPress: @escaping (() async -> Void)) {
self.title = title
self.message = message
self.buttonTitle = buttonTitle

View file

@ -81,7 +81,7 @@ public enum SheetDestination: Identifiable, Hashable {
public var id: String {
switch self {
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
.mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor, .imageURL:
.mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor, .imageURL:
"statusEditor"
case .listCreate:
"listCreate"
@ -177,9 +177,10 @@ public enum SheetDestination: Identifiable, Hashable {
}
return .handled
} else if let client,
client.isAuth,
client.hasConnection(with: url),
let id = Int(url.lastPathComponent) {
client.isAuth,
client.hasConnection(with: url),
let id = Int(url.lastPathComponent)
{
if url.absoluteString.contains(client.server) {
navigate(to: .statusDetail(id: String(id)))
} else {
@ -193,7 +194,8 @@ public enum SheetDestination: Identifiable, Hashable {
public func handleDeepLink(url: URL) -> OpenURLAction.Result {
guard let client,
client.isAuth,
let id = Int(url.lastPathComponent) else {
let id = Int(url.lastPathComponent)
else {
return urlHandler?(url) ?? .systemAction
}
// First check whether we already know that the client's server federates with the server this post is on

View file

@ -1,7 +1,7 @@
import SwiftUI
import Models
import DesignSystem
import Env
import Models
import SwiftUI
struct NotificationsHeaderFilteredView: View {
@Environment(Theme.self) private var theme

View file

@ -24,7 +24,8 @@ public struct NotificationsListView: View {
public init(lockedType: Models.Notification.NotificationType? = nil,
lockedAccountId: String? = nil,
scrollToTopSignal: Binding<Int>) {
scrollToTopSignal: Binding<Int>)
{
self.lockedType = lockedType
self.lockedAccountId = lockedAccountId
_scrollToTopSignal = scrollToTopSignal
@ -113,7 +114,7 @@ public struct NotificationsListView: View {
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.onAppear {
.onAppear {
viewModel.client = client
viewModel.currentAccount = account
if let lockedType {

View file

@ -1,7 +1,7 @@
import SwiftUI
import Network
import DesignSystem
import Models
import Network
import SwiftUI
@MainActor
struct NotificationsPolicyView: View {
@ -19,32 +19,32 @@ struct NotificationsPolicyView: View {
Section("notifications.content-filter.title-inline") {
Toggle(isOn: .init(get: { policy?.filterNotFollowing == true },
set: { newValue in
policy?.filterNotFollowing = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleYouDontFollow")
})
policy?.filterNotFollowing = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleYouDontFollow")
})
Toggle(isOn: .init(get: { policy?.filterNotFollowers == true },
set: { newValue in
policy?.filterNotFollowers = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleNotFollowingYou")
})
policy?.filterNotFollowers = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.peopleNotFollowingYou")
})
Toggle(isOn: .init(get: { policy?.filterNewAccounts == true },
set: { newValue in
policy?.filterNewAccounts = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.newAccounts")
})
policy?.filterNewAccounts = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.newAccounts")
})
Toggle(isOn: .init(get: { policy?.filterPrivateMentions == true },
set: { newValue in
policy?.filterPrivateMentions = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.privateMentions")
})
policy?.filterPrivateMentions = newValue
Task { await updatePolicy() }
}), label: {
Text("notifications.content-filter.privateMentions")
})
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
@ -84,7 +84,7 @@ struct NotificationsPolicyView: View {
do {
isUpdating = true
self.policy = try await client.put(endpoint: Notifications.putPolicy(policy: policy))
} catch { }
} catch {}
}
}
}

View file

@ -39,7 +39,7 @@ import SwiftUI
private let filterKey = "notification-filter"
var state: State = .loading
var isLockedType: Bool = false
var lockedAccountId: String? = nil
var lockedAccountId: String?
var policy: Models.NotificationsPolicy?
var selectedType: Models.Notification.NotificationType? {
didSet {
@ -156,7 +156,7 @@ import SwiftUI
let newNotifications: [Models.Notification]
if let lockedAccountId {
newNotifications =
try await client.get(endpoint: Notifications.notificationsForAccount(accountId: lockedAccountId, maxId: lastId))
try await client.get(endpoint: Notifications.notificationsForAccount(accountId: lockedAccountId, maxId: lastId))
} else {
newNotifications =
try await client.get(endpoint: Notifications.notifications(minId: nil,

View file

@ -1,7 +1,7 @@
import SwiftUI
import Network
import Models
import DesignSystem
import Models
import Network
import SwiftUI
@MainActor
public struct NotificationsRequestsListView: View {
@ -13,18 +13,19 @@ public struct NotificationsRequestsListView: View {
case error
case requests(_ data: [NotificationsRequest])
}
@State private var viewState: ViewState = .loading
public init() { }
public init() {}
public var body: some View {
List {
switch viewState {
case .loading:
ProgressView()
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
#endif
.listSectionSeparator(.hidden)
case .error:
ErrorView(title: "notifications.error.title",
@ -33,10 +34,10 @@ public struct NotificationsRequestsListView: View {
{
await fetchRequests()
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
.listSectionSeparator(.hidden)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
.listSectionSeparator(.hidden)
case let .requests(data):
ForEach(data) { request in
NotificationsRequestsRowView(request: request)
@ -59,22 +60,22 @@ public struct NotificationsRequestsListView: View {
}
.listStyle(.plain)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.navigationTitle("notifications.content-filter.requests.title")
.navigationBarTitleDisplayMode(.inline)
.task {
await fetchRequests()
}
.refreshable {
await fetchRequests()
}
.navigationTitle("notifications.content-filter.requests.title")
.navigationBarTitleDisplayMode(.inline)
.task {
await fetchRequests()
}
.refreshable {
await fetchRequests()
}
}
private func fetchRequests() async {
do {
viewState = .requests(try await client.get(endpoint: Notifications.requests))
viewState = try .requests(await client.get(endpoint: Notifications.requests))
} catch {
viewState = .error
}

View file

@ -1,8 +1,8 @@
import SwiftUI
import Models
import DesignSystem
import Env
import Models
import Network
import SwiftUI
struct NotificationsRequestsRowView: View {
@Environment(Theme.self) private var theme
@ -50,7 +50,7 @@ struct NotificationsRequestsRowView: View {
.listRowBackground(RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.background))
#else
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
}

View file

@ -73,74 +73,74 @@ public extension StatusEditor {
.scrollPosition(id: $scrollID, anchor: .top)
.animation(.bouncy(duration: 0.3), value: editorFocusState)
.animation(.bouncy(duration: 0.3), value: followUpSEVMs)
#if !os(visionOS)
.background(theme.primaryBackgroundColor)
#endif
.safeAreaInset(edge: .bottom) {
AutoCompleteView(viewModel: focusedSEVM)
}
#if os(visionOS)
.ornament(attachmentAnchor: .scene(.leading)) {
AccessoryView(focusedSEVM: focusedSEVM,
followUpSEVMs: $followUpSEVMs)
}
#else
.safeAreaInset(edge: .bottom) {
if presentationDetent == .large || presentationDetent == .medium {
#if !os(visionOS)
.background(theme.primaryBackgroundColor)
#endif
.safeAreaInset(edge: .bottom) {
AutoCompleteView(viewModel: focusedSEVM)
}
#if os(visionOS)
.ornament(attachmentAnchor: .scene(.leading)) {
AccessoryView(focusedSEVM: focusedSEVM,
followUpSEVMs: $followUpSEVMs)
}
}
#endif
.accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views
.navigationTitle(focusedSEVM.mode.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItems(mainSEVM: mainSEVM,
focusedSEVM: focusedSEVM,
followUpSEVMs: followUpSEVMs) }
.toolbarBackground(.visible, for: .navigationBar)
.alert(
"status.error.posting.title",
isPresented: $focusedSEVM.showPostingErrorAlert,
actions: {
Button("OK") {}
}, message: {
Text(mainSEVM.postingError ?? "")
}
)
.interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning)
.onChange(of: appAccounts.currentClient) { _, newValue in
if mainSEVM.mode.isInShareExtension {
currentAccount.setClient(client: newValue)
mainSEVM.client = newValue
for post in followUpSEVMs {
post.client = newValue
}
}
}
.onDrop(of: [.image, .video, .gif, .mpeg4Movie, .quickTimeMovie, .movie],
delegate: focusedSEVM)
.onChange(of: currentAccount.account?.id) {
mainSEVM.currentAccount = currentAccount.account
for p in followUpSEVMs {
p.currentAccount = mainSEVM.currentAccount
}
}
.onChange(of: mainSEVM.visibility) {
for p in followUpSEVMs {
p.visibility = mainSEVM.visibility
}
}
.onChange(of: followUpSEVMs.count) { oldValue, newValue in
if oldValue < newValue {
Task {
try? await Task.sleep(for: .seconds(0.1))
withAnimation(.bouncy(duration: 0.5)) {
scrollID = followUpSEVMs.last?.id
#else
.safeAreaInset(edge: .bottom) {
if presentationDetent == .large || presentationDetent == .medium {
AccessoryView(focusedSEVM: focusedSEVM,
followUpSEVMs: $followUpSEVMs)
}
}
#endif
.accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views
.navigationTitle(focusedSEVM.mode.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItems(mainSEVM: mainSEVM,
focusedSEVM: focusedSEVM,
followUpSEVMs: followUpSEVMs) }
.toolbarBackground(.visible, for: .navigationBar)
.alert(
"status.error.posting.title",
isPresented: $focusedSEVM.showPostingErrorAlert,
actions: {
Button("OK") {}
}, message: {
Text(mainSEVM.postingError ?? "")
}
)
.interactiveDismissDisabled(mainSEVM.shouldDisplayDismissWarning)
.onChange(of: appAccounts.currentClient) { _, newValue in
if mainSEVM.mode.isInShareExtension {
currentAccount.setClient(client: newValue)
mainSEVM.client = newValue
for post in followUpSEVMs {
post.client = newValue
}
}
}
.onDrop(of: [.image, .video, .gif, .mpeg4Movie, .quickTimeMovie, .movie],
delegate: focusedSEVM)
.onChange(of: currentAccount.account?.id) {
mainSEVM.currentAccount = currentAccount.account
for p in followUpSEVMs {
p.currentAccount = mainSEVM.currentAccount
}
}
.onChange(of: mainSEVM.visibility) {
for p in followUpSEVMs {
p.visibility = mainSEVM.visibility
}
}
.onChange(of: followUpSEVMs.count) { oldValue, newValue in
if oldValue < newValue {
Task {
try? await Task.sleep(for: .seconds(0.1))
withAnimation(.bouncy(duration: 0.5)) {
scrollID = followUpSEVMs.last?.id
}
}
}
}
}
}
if mainSEVM.isPosting {
ProgressView(value: mainSEVM.postingProgress, total: 100.0)
}

View file

@ -313,7 +313,7 @@ public extension StatusEditor {
processItemsProvider(items: items)
case let .imageURL(urls, visibility):
Task {
for container in await Self.makeImageContainer(from: urls) {
for container in await Self.makeImageContainer(from: urls) {
prepareToPost(for: container)
}
}
@ -754,8 +754,8 @@ public extension StatusEditor {
let compressor = Compressor()
if let compressedData = await compressor.compressImageFrom(url: url),
let image = UIImage(data: compressedData) {
let image = UIImage(data: compressedData)
{
containers.append(MediaContainer(
id: UUID().uuidString,
image: image,