) {
- _scrollToTopSignal = scrollToTopSignal
- }
+ public init() { }
public var body: some View {
ScrollViewReader { proxy in
@@ -111,15 +107,6 @@ public struct ExploreView: View {
.task(id: viewModel.searchQuery) {
await viewModel.search()
}
- .onChange(of: scrollToTopSignal) {
- if viewModel.scrollToTopVisible {
- viewModel.isSearchPresented.toggle()
- } else {
- withAnimation {
- proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
- }
- }
- }
}
}
diff --git a/Packages/Lists/Package.swift b/Packages/Lists/Package.swift
index 403f79207..37d3c2a4e 100644
--- a/Packages/Lists/Package.swift
+++ b/Packages/Lists/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -34,7 +34,7 @@ let package = Package(
.product(name: "DesignSystem", package: "DesignSystem"),
],
swiftSettings: [
- .enableExperimentalFeature("StrictConcurrency"),
+ .swiftLanguageMode(.v6),
]
),
]
diff --git a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift
index 344263da4..ad2eed09a 100644
--- a/Packages/Lists/Sources/Lists/Create/ListCreateView.swift
+++ b/Packages/Lists/Sources/Lists/Create/ListCreateView.swift
@@ -42,6 +42,7 @@ public struct ListCreateView: View {
CancelToolbarItem()
ToolbarItem {
Button {
+ let client = client
Task {
isSaving = true
let _: Models.List = try await client.post(endpoint: Lists.createList(title: title,
diff --git a/Packages/MediaUI/Package.swift b/Packages/MediaUI/Package.swift
index ba6c1e042..127455502 100644
--- a/Packages/MediaUI/Package.swift
+++ b/Packages/MediaUI/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -28,7 +28,7 @@ let package = Package(
.product(name: "DesignSystem", package: "DesignSystem"),
],
swiftSettings: [
- .enableExperimentalFeature("StrictConcurrency"),
+ .swiftLanguageMode(.v6),
]
),
]
diff --git a/Packages/Models/Package.swift b/Packages/Models/Package.swift
index d8edd7e4d..0a95e5da2 100644
--- a/Packages/Models/Package.swift
+++ b/Packages/Models/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -26,7 +26,7 @@ let package = Package(
"SwiftSoup",
],
swiftSettings: [
- .enableExperimentalFeature("StrictConcurrency"),
+ .swiftLanguageMode(.v6),
]
),
.testTarget(
diff --git a/Packages/Models/Sources/Models/Stream/StreamEvent.swift b/Packages/Models/Sources/Models/Stream/StreamEvent.swift
index 1ba79d7d0..c2a4dd6e7 100644
--- a/Packages/Models/Sources/Models/Stream/StreamEvent.swift
+++ b/Packages/Models/Sources/Models/Stream/StreamEvent.swift
@@ -1,17 +1,17 @@
import Foundation
-public struct RawStreamEvent: Decodable {
+public struct RawStreamEvent: Decodable, Sendable{
public let event: String
public let stream: [String]
public let payload: String
}
-public protocol StreamEvent: Identifiable {
+public protocol StreamEvent: Identifiable, Sendable {
var date: Date { get }
var id: String { get }
}
-public struct StreamEventUpdate: StreamEvent {
+public struct StreamEventUpdate: StreamEvent, Sendable {
public let date = Date()
public var id: String { status.id }
public let status: Status
@@ -20,7 +20,7 @@ public struct StreamEventUpdate: StreamEvent {
}
}
-public struct StreamEventStatusUpdate: StreamEvent {
+public struct StreamEventStatusUpdate: StreamEvent, Sendable {
public let date = Date()
public var id: String { status.id + (status.editedAt?.asDate.description ?? "") }
public let status: Status
@@ -29,7 +29,7 @@ public struct StreamEventStatusUpdate: StreamEvent {
}
}
-public struct StreamEventDelete: StreamEvent {
+public struct StreamEventDelete: StreamEvent, Sendable {
public let date = Date()
public var id: String { status + date.description }
public let status: String
@@ -38,7 +38,7 @@ public struct StreamEventDelete: StreamEvent {
}
}
-public struct StreamEventNotification: StreamEvent {
+public struct StreamEventNotification: StreamEvent, Sendable {
public let date = Date()
public var id: String { notification.id }
public let notification: Notification
@@ -47,7 +47,7 @@ public struct StreamEventNotification: StreamEvent {
}
}
-public struct StreamEventConversation: StreamEvent {
+public struct StreamEventConversation: StreamEvent, Sendable {
public let date = Date()
public var id: String { conversation.id }
public let conversation: Conversation
diff --git a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift
index 1f674cd38..1bf3ab5ba 100644
--- a/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift
+++ b/Packages/Models/Tests/ModelsTests/HTMLStringTests.swift
@@ -1,88 +1,88 @@
@testable import Models
-import XCTest
+import Testing
+import Foundation
-final class HTMLStringTests: XCTestCase {
- func testURLInit() throws {
- XCTAssertNil(URL(string: "", encodePath: true))
+@Test
+func testURLInit() throws {
+ let simpleUrl = URL(string: "https://www.google.com", encodePath: true)
+ #expect("https://www.google.com" == simpleUrl?.absoluteString)
- let simpleUrl = URL(string: "https://www.google.com", encodePath: true)
- XCTAssertEqual("https://www.google.com", simpleUrl?.absoluteString)
+ let urlWithTrailingSlash = URL(string: "https://www.google.com/", encodePath: true)
+ #expect("https://www.google.com/" == urlWithTrailingSlash?.absoluteString)
- let urlWithTrailingSlash = URL(string: "https://www.google.com/", encodePath: true)
- XCTAssertEqual("https://www.google.com/", urlWithTrailingSlash?.absoluteString)
+ let extendedCharPath = URL(string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: true)
+ #expect("https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station" == extendedCharPath?.absoluteString)
- let extendedCharPath = URL(string: "https://en.wikipedia.org/wiki/Elbbrücken_station", encodePath: true)
- XCTAssertEqual("https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", extendedCharPath?.absoluteString)
+ let extendedCharQuery = URL(string: "http://test.com/blah/city?name=京都市", encodePath: true)
+ #expect("http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82" == extendedCharQuery?.absoluteString)
- let extendedCharQuery = URL(string: "http://test.com/blah/city?name=京都市", encodePath: true)
- XCTAssertEqual("http://test.com/blah/city?name=%E4%BA%AC%E9%83%BD%E5%B8%82", extendedCharQuery?.absoluteString)
-
- // Double encoding will happen if you ask to encodePath on an already encoded string
- let alreadyEncodedPath = URL(string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true)
- XCTAssertEqual("https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station", alreadyEncodedPath?.absoluteString)
- }
+ // Double encoding will happen if you ask to encodePath on an already encoded string
+ let alreadyEncodedPath = URL(string: "https://en.wikipedia.org/wiki/Elbbr%C3%BCcken_station", encodePath: true)
+ #expect("https://en.wikipedia.org/wiki/Elbbr%25C3%25BCcken_station" == alreadyEncodedPath?.absoluteString)
+}
- func testHTMLStringInit() throws {
- let decoder = JSONDecoder()
+@Test
+func testHTMLStringInit() throws {
+ let decoder = JSONDecoder()
- let basicContent = "\"This is a test
\""
- var htmlString = try decoder.decode(HTMLString.self, from: Data(basicContent.utf8))
- XCTAssertEqual("This is a test", htmlString.asRawText)
- XCTAssertEqual("This is a test
", htmlString.htmlValue)
- XCTAssertEqual("This is a test", htmlString.asMarkdown)
- XCTAssertEqual(0, htmlString.statusesURLs.count)
- XCTAssertEqual(0, htmlString.links.count)
+ let basicContent = "\"This is a test
\""
+ var htmlString = try decoder.decode(HTMLString.self, from: Data(basicContent.utf8))
+ #expect("This is a test" == htmlString.asRawText)
+ #expect("This is a test
" == htmlString.htmlValue)
+ #expect("This is a test" == htmlString.asMarkdown)
+ #expect(0 == htmlString.statusesURLs.count)
+ #expect(0 == htmlString.links.count)
- let basicLink = "\"This is a test
\""
- htmlString = try decoder.decode(HTMLString.self, from: Data(basicLink.utf8))
- XCTAssertEqual("This is a test", htmlString.asRawText)
- XCTAssertEqual("This is a test
", htmlString.htmlValue)
- XCTAssertEqual("This is a [test](https://test.com)", htmlString.asMarkdown)
- XCTAssertEqual(0, htmlString.statusesURLs.count)
- XCTAssertEqual(1, htmlString.links.count)
- XCTAssertEqual("https://test.com", htmlString.links[0].url.absoluteString)
- XCTAssertEqual("test", htmlString.links[0].displayString)
+ let basicLink = "\"This is a test
\""
+ htmlString = try decoder.decode(HTMLString.self, from: Data(basicLink.utf8))
+ #expect("This is a test" == htmlString.asRawText)
+ #expect("This is a test
" == htmlString.htmlValue)
+ #expect("This is a [test](https://test.com)" == htmlString.asMarkdown)
+ #expect(0 == htmlString.statusesURLs.count)
+ #expect(1 == htmlString.links.count)
+ #expect("https://test.com" == htmlString.links[0].url.absoluteString)
+ #expect("test" == htmlString.links[0].displayString)
- let extendedCharLink = "\"This is a test
\""
- htmlString = try decoder.decode(HTMLString.self, from: Data(extendedCharLink.utf8))
- XCTAssertEqual("This is a test", htmlString.asRawText)
- XCTAssertEqual("This is a test
", htmlString.htmlValue)
- XCTAssertEqual("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)", htmlString.asMarkdown)
- XCTAssertEqual(0, htmlString.statusesURLs.count)
- XCTAssertEqual(1, htmlString.links.count)
- XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString)
- XCTAssertEqual("test", htmlString.links[0].displayString)
+ let extendedCharLink = "\"This is a test
\""
+ htmlString = try decoder.decode(HTMLString.self, from: Data(extendedCharLink.utf8))
+ #expect("This is a test" == htmlString.asRawText)
+ #expect("This is a test
" == htmlString.htmlValue)
+ #expect("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)" == htmlString.asMarkdown)
+ #expect(0 == htmlString.statusesURLs.count)
+ #expect(1 == htmlString.links.count)
+ #expect("https://test.com/go%C3%9F%C3%AB%C3%B1a" == htmlString.links[0].url.absoluteString)
+ #expect("test" == htmlString.links[0].displayString)
- let alreadyEncodedLink = "\"This is a test
\""
- htmlString = try decoder.decode(HTMLString.self, from: Data(alreadyEncodedLink.utf8))
- XCTAssertEqual("This is a test", htmlString.asRawText)
- XCTAssertEqual("This is a test
", htmlString.htmlValue)
- XCTAssertEqual("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)", htmlString.asMarkdown)
- XCTAssertEqual(0, htmlString.statusesURLs.count)
- XCTAssertEqual(1, htmlString.links.count)
- XCTAssertEqual("https://test.com/go%C3%9F%C3%AB%C3%B1a", htmlString.links[0].url.absoluteString)
- XCTAssertEqual("test", htmlString.links[0].displayString)
- }
+ let alreadyEncodedLink = "\"This is a test
\""
+ htmlString = try decoder.decode(HTMLString.self, from: Data(alreadyEncodedLink.utf8))
+ #expect("This is a test" == htmlString.asRawText)
+ #expect("This is a test
" == htmlString.htmlValue)
+ #expect("This is a [test](https://test.com/go%C3%9F%C3%AB%C3%B1a)" == htmlString.asMarkdown)
+ #expect(0 == htmlString.statusesURLs.count)
+ #expect(1 == htmlString.links.count)
+ #expect("https://test.com/go%C3%9F%C3%AB%C3%B1a" == htmlString.links[0].url.absoluteString)
+ #expect("test" == htmlString.links[0].displayString)
+}
- func testHTMLStringInit_markdownEscaping() throws {
- let decoder = JSONDecoder()
+@Test
+func testHTMLStringInit_markdownEscaping() throws {
+ let decoder = JSONDecoder()
- let stdMarkdownContent = "\"This [*is*] `a`\\n**test**
\""
- var htmlString = try decoder.decode(HTMLString.self, from: Data(stdMarkdownContent.utf8))
- XCTAssertEqual("This [*is*] `a`\n**test**", htmlString.asRawText)
- XCTAssertEqual("This [*is*] `a`\n**test**
", htmlString.htmlValue)
- XCTAssertEqual("This \\[\\*is\\*] \\`a\\` \\*\\*test\\*\\*", htmlString.asMarkdown)
+ let stdMarkdownContent = "\"This [*is*] `a`\\n**test**
\""
+ var htmlString = try decoder.decode(HTMLString.self, from: Data(stdMarkdownContent.utf8))
+ #expect("This [*is*] `a`\n**test**" == htmlString.asRawText)
+ #expect("This [*is*] `a`\n**test**
" == htmlString.htmlValue)
+ #expect("This \\[\\*is\\*] \\`a\\` \\*\\*test\\*\\*" == htmlString.asMarkdown)
- let underscoreContent = "\"This _is_ an :emoji_maybe:
\""
- htmlString = try decoder.decode(HTMLString.self, from: Data(underscoreContent.utf8))
- XCTAssertEqual("This _is_ an :emoji_maybe:", htmlString.asRawText)
- XCTAssertEqual("This _is_ an :emoji_maybe:
", htmlString.htmlValue)
- XCTAssertEqual("This \\_is\\_ an :emoji_maybe:", htmlString.asMarkdown)
+ let underscoreContent = "\"This _is_ an :emoji_maybe:
\""
+ htmlString = try decoder.decode(HTMLString.self, from: Data(underscoreContent.utf8))
+ #expect("This _is_ an :emoji_maybe:" == htmlString.asRawText)
+ #expect("This _is_ an :emoji_maybe:
" == htmlString.htmlValue)
+ #expect("This \\_is\\_ an :emoji_maybe:" == htmlString.asMarkdown)
- let strikeContent = "\"This ~is~ a\\n`test`
\""
- htmlString = try decoder.decode(HTMLString.self, from: Data(strikeContent.utf8))
- XCTAssertEqual("This ~is~ a\n`test`", htmlString.asRawText)
- XCTAssertEqual("This ~is~ a\n`test`
", htmlString.htmlValue)
- XCTAssertEqual("This \\~is\\~ a \\`test\\`", htmlString.asMarkdown)
- }
+ let strikeContent = "\"This ~is~ a\\n`test`
\""
+ htmlString = try decoder.decode(HTMLString.self, from: Data(strikeContent.utf8))
+ #expect("This ~is~ a\n`test`" == htmlString.asRawText)
+ #expect("This ~is~ a\n`test`
" == htmlString.htmlValue)
+ #expect("This \\~is\\~ a \\`test\\`" == htmlString.asMarkdown)
}
diff --git a/Packages/Network/Package.swift b/Packages/Network/Package.swift
index 0c8e01fec..b9ccc459d 100644
--- a/Packages/Network/Package.swift
+++ b/Packages/Network/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -26,7 +26,7 @@ let package = Package(
.product(name: "Models", package: "Models"),
],
swiftSettings: [
- .enableExperimentalFeature("StrictConcurrency"),
+ .swiftLanguageMode(.v6),
]
),
.testTarget(
diff --git a/Packages/Notifications/Package.swift b/Packages/Notifications/Package.swift
index 149a24338..2727b9322 100644
--- a/Packages/Notifications/Package.swift
+++ b/Packages/Notifications/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -34,7 +34,7 @@ let package = Package(
.product(name: "DesignSystem", package: "DesignSystem"),
],
swiftSettings: [
- .enableExperimentalFeature("StrictConcurrency"),
+ .swiftLanguageMode(.v6),
]
),
]
diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift
index a90f82f2a..65d2f3a92 100644
--- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift
+++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift
@@ -17,39 +17,29 @@ public struct NotificationsListView: View {
@State private var viewModel = NotificationsViewModel()
@State private var isNotificationsPolicyPresented: Bool = false
- @Binding var scrollToTopSignal: Int
let lockedType: Models.Notification.NotificationType?
let lockedAccountId: String?
public init(lockedType: Models.Notification.NotificationType? = nil,
- lockedAccountId: String? = nil,
- scrollToTopSignal: Binding)
+ lockedAccountId: String? = nil)
{
self.lockedType = lockedType
self.lockedAccountId = lockedAccountId
- _scrollToTopSignal = scrollToTopSignal
}
public var body: some View {
- ScrollViewReader { proxy in
- List {
- scrollToTopView
- topPaddingView
- if lockedAccountId == nil, let summary = viewModel.policy?.summary {
- NotificationsHeaderFilteredView(filteredNotifications: summary)
- }
- notificationsView
- }
- .id(account.account?.id)
- .environment(\.defaultMinListRowHeight, 1)
- .listStyle(.plain)
- .onChange(of: scrollToTopSignal) {
- withAnimation {
- proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
- }
+ List {
+ scrollToTopView
+ topPaddingView
+ if lockedAccountId == nil, let summary = viewModel.policy?.summary {
+ NotificationsHeaderFilteredView(filteredNotifications: summary)
}
+ notificationsView
}
+ .id(account.account?.id)
+ .environment(\.defaultMinListRowHeight, 1)
+ .listStyle(.plain)
.toolbar {
ToolbarItem(placement: .principal) {
let title = lockedType?.menuTitle() ?? viewModel.selectedType?.menuTitle() ?? "notifications.navigation-title"
@@ -75,7 +65,7 @@ public struct NotificationsListView: View {
Button {
viewModel.selectedType = nil
Task {
- await viewModel.fetchNotifications()
+ await viewModel.fetchNotifications(viewModel.selectedType)
}
} label: {
Label("notifications.navigation-title", systemImage: "bell.fill")
@@ -85,7 +75,7 @@ public struct NotificationsListView: View {
Button {
viewModel.selectedType = type
Task {
- await viewModel.fetchNotifications()
+ await viewModel.fetchNotifications(viewModel.selectedType)
}
} label: {
Label {
@@ -126,27 +116,27 @@ public struct NotificationsListView: View {
viewModel.loadSelectedType()
}
Task {
- await viewModel.fetchNotifications()
+ await viewModel.fetchNotifications(viewModel.selectedType)
await viewModel.fetchPolicy()
}
}
.refreshable {
SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
- await viewModel.fetchNotifications()
+ await viewModel.fetchNotifications(viewModel.selectedType)
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(.refresh)
}
.onChange(of: watcher.latestEvent?.id) {
if let latestEvent = watcher.latestEvent {
- viewModel.handleEvent(event: latestEvent)
+ viewModel.handleEvent(selectedType: viewModel.selectedType, event: latestEvent)
}
}
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:
Task {
- await viewModel.fetchNotifications()
+ await viewModel.fetchNotifications(viewModel.selectedType)
}
default:
break
@@ -212,7 +202,7 @@ public struct NotificationsListView: View {
EmptyView()
case .hasNextPage:
NextPageView {
- try await viewModel.fetchNextPage()
+ try await viewModel.fetchNextPage(viewModel.selectedType)
}
.listRowInsets(.init(top: .layoutPadding,
leading: .layoutPadding + 4,
@@ -229,7 +219,7 @@ public struct NotificationsListView: View {
message: "notifications.error.message",
buttonTitle: "action.retry")
{
- await viewModel.fetchNotifications()
+ await viewModel.fetchNotifications(viewModel.selectedType)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift
index 5197e413f..33eff9d7e 100644
--- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift
+++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift
@@ -78,7 +78,7 @@ import SwiftUI
private var consolidatedNotifications: [ConsolidatedNotification] = []
- func fetchNotifications() async {
+ func fetchNotifications(_ selectedType: Models.Notification.NotificationType?) async {
guard let client, let currentAccount else { return }
do {
var nextPageState: State.PagingState = .hasNextPage
@@ -150,7 +150,7 @@ import SwiftUI
return allNotifications
}
- func fetchNextPage() async throws {
+ func fetchNextPage(_ selectedType: Models.Notification.NotificationType?) async throws {
guard let client else { return }
guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return }
let newNotifications: [Models.Notification]
@@ -185,7 +185,7 @@ import SwiftUI
policy = try? await client?.get(endpoint: Notifications.policy)
}
- func handleEvent(event: any StreamEvent) {
+ func handleEvent(selectedType: Models.Notification.NotificationType?, event: any StreamEvent) {
Task {
// Check if the event is a notification,
// if it is not already in the list,
diff --git a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift
index 65ddb46c7..3ef60f8a6 100644
--- a/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift
+++ b/Packages/Notifications/Sources/Notifications/Requests/NotificationsRequestsListView.swift
@@ -32,7 +32,7 @@ public struct NotificationsRequestsListView: View {
message: "notifications.error.message",
buttonTitle: "action.retry")
{
- await fetchRequests()
+ await fetchRequests(client)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
@@ -43,13 +43,13 @@ public struct NotificationsRequestsListView: View {
NotificationsRequestsRowView(request: request)
.swipeActions {
Button {
- Task { await acceptRequest(request) }
+ Task { await acceptRequest(client, request) }
} label: {
Label("account.follow-request.accept", systemImage: "checkmark")
}
Button {
- Task { await dismissRequest(request) }
+ Task { await dismissRequest(client, request) }
} label: {
Label("account.follow-request.reject", systemImage: "xmark")
}
@@ -66,14 +66,14 @@ public struct NotificationsRequestsListView: View {
.navigationTitle("notifications.content-filter.requests.title")
.navigationBarTitleDisplayMode(.inline)
.task {
- await fetchRequests()
+ await fetchRequests(client)
}
.refreshable {
- await fetchRequests()
+ await fetchRequests(client)
}
}
- private func fetchRequests() async {
+ private func fetchRequests(_ client: Client) async {
do {
viewState = try .requests(await client.get(endpoint: Notifications.requests))
} catch {
@@ -81,13 +81,13 @@ public struct NotificationsRequestsListView: View {
}
}
- private func acceptRequest(_ request: NotificationsRequest) async {
+ private func acceptRequest(_ client: Client, _ request: NotificationsRequest) async {
_ = try? await client.post(endpoint: Notifications.acceptRequest(id: request.id))
- await fetchRequests()
+ await fetchRequests(client)
}
- private func dismissRequest(_ request: NotificationsRequest) async {
+ private func dismissRequest(_ client: Client, _ request: NotificationsRequest) async {
_ = try? await client.post(endpoint: Notifications.dismissRequest(id: request.id))
- await fetchRequests()
+ await fetchRequests(client)
}
}
diff --git a/Packages/StatusKit/Package.swift b/Packages/StatusKit/Package.swift
index f20bdab30..3f2575e59 100644
--- a/Packages/StatusKit/Package.swift
+++ b/Packages/StatusKit/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -38,7 +38,7 @@ let package = Package(
.product(name: "LRUCache", package: "LRUCache"),
],
swiftSettings: [
- .enableExperimentalFeature("StrictConcurrency"),
+ .swiftLanguageMode(.v6),
]
),
]
diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift
index 054b07428..7c8467ed7 100644
--- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift
+++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/GIF/GIFPickerView.swift
@@ -1,9 +1,10 @@
#if !os(visionOS) && !DEBUG
import DesignSystem
- import GiphyUISDK
+ @preconcurrency import GiphyUISDK
import SwiftUI
import UIKit
-
+
+ @MainActor
struct GifPickerView: UIViewControllerRepresentable {
@Environment(Theme.self) private var theme
@@ -33,6 +34,7 @@
GifPickerView.Coordinator(parent: self)
}
+ @MainActor
class Coordinator: NSObject, GiphyDelegate {
var parent: GifPickerView
@@ -40,13 +42,17 @@
self.parent = parent
}
- func didDismiss(controller _: GiphyViewController?) {
- parent.onShouldDismissGifPicker()
+ nonisolated func didDismiss(controller _: GiphyViewController?) {
+ Task { @MainActor in
+ parent.onShouldDismissGifPicker()
+ }
}
- func didSelectMedia(giphyViewController _: GiphyViewController, media: GPHMedia) {
- let url = media.url(rendition: .fixedWidth, fileType: .gif)
- parent.completion(url ?? "")
+ nonisolated func didSelectMedia(giphyViewController _: GiphyViewController, media: GPHMedia) {
+ Task { @MainActor in
+ let url = media.url(rendition: .fixedWidth, fileType: .gif)
+ parent.completion(url ?? "")
+ }
}
}
}
diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift
index fc158a3cc..d86915458 100644
--- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift
+++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/UTTypeSupported.swift
@@ -1,5 +1,5 @@
-@preconcurrency import AVFoundation
-import Foundation
+import AVFoundation
+@preconcurrency import Foundation
import PhotosUI
import SwiftUI
import UIKit
diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift
index 5f570f39b..39c8f26a6 100644
--- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift
+++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift
@@ -17,7 +17,7 @@ public extension StatusEditor {
var client: Client?
var currentAccount: Account? {
didSet {
- if let itemsProvider {
+ if itemsProvider != nil {
mediaContainers = []
}
}
@@ -981,4 +981,4 @@ extension StatusEditor.ViewModel: UITextPasteDelegate {
}
}
-extension PhotosPickerItem: @unchecked Sendable {}
+extension PhotosPickerItem: @unchecked @retroactive Sendable {}
diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift
index 7cc4d2f8c..92dab792b 100644
--- a/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift
+++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusActionButtonStyle.swift
@@ -45,7 +45,8 @@ struct StatusActionButtonStyle: ButtonStyle {
}
}
- struct SparklesView: View, Animatable {
+ @MainActor
+ struct SparklesView: View, @preconcurrency Animatable {
var counter: Float
var tint: Color
var size: CGFloat
diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift
index 222f17698..04da7a45c 100644
--- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift
+++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift
@@ -66,7 +66,7 @@ struct StatusRowTranslateView: View {
generalTranslateButton
.onChange(of: preferences.preferredTranslationType) { _, _ in
withAnimation {
- _ = viewModel.updatePreferredTranslation()
+ viewModel.updatePreferredTranslation()
}
}
diff --git a/Packages/Timeline/Package.swift b/Packages/Timeline/Package.swift
index b0c48db0d..258f5203d 100644
--- a/Packages/Timeline/Package.swift
+++ b/Packages/Timeline/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -38,7 +38,7 @@ let package = Package(
.product(name: "Bodega", package: "Bodega"),
],
swiftSettings: [
- .enableExperimentalFeature("StrictConcurrency"),
+ .swiftLanguageMode(.v6),
]
),
.testTarget(
diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift
index 47db3c4d6..e759d6607 100644
--- a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift
+++ b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift
@@ -27,7 +27,6 @@ public struct TimelineView: View {
@Binding var timeline: TimelineFilter
@Binding var pinnedFilters: [TimelineFilter]
@Binding var selectedTagGroup: TagGroup?
- @Binding var scrollToTopSignal: Int
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
@@ -36,82 +35,73 @@ public struct TimelineView: View {
public init(timeline: Binding,
pinnedFilters: Binding<[TimelineFilter]>,
selectedTagGroup: Binding,
- scrollToTopSignal: Binding,
canFilterTimeline: Bool)
{
_timeline = timeline
_pinnedFilters = pinnedFilters
_selectedTagGroup = selectedTagGroup
- _scrollToTopSignal = scrollToTopSignal
self.canFilterTimeline = canFilterTimeline
}
public var body: some View {
- ScrollViewReader { proxy in
- ZStack(alignment: .top) {
- List {
- scrollToTopView
- TimelineTagGroupheaderView(group: $selectedTagGroup, timeline: $timeline)
- TimelineTagHeaderView(tag: $viewModel.tag)
- switch viewModel.timeline {
- case .remoteLocal:
- StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true)
- default:
- StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
- .environment(\.isHomeTimeline, timeline == .home)
- }
- }
- .id(client.id)
- .environment(\.defaultMinListRowHeight, 1)
- .listStyle(.plain)
- #if !os(visionOS)
- .scrollContentBackground(.hidden)
- .background(theme.primaryBackgroundColor)
- #endif
- .introspect(.list, on: .iOS(.v17, .v18)) { (collectionView: UICollectionView) in
- DispatchQueue.main.async {
- self.collectionView = collectionView
- }
- prefetcher.viewModel = viewModel
- collectionView.isPrefetchingEnabled = true
- collectionView.prefetchDataSource = prefetcher
- }
- if viewModel.timeline.supportNewestPagination {
- TimelineUnreadStatusesView(observer: viewModel.pendingStatusesObserver)
+ ZStack(alignment: .top) {
+ List {
+ scrollToTopView
+ TimelineTagGroupheaderView(group: $selectedTagGroup, timeline: $timeline)
+ TimelineTagHeaderView(tag: $viewModel.tag)
+ switch viewModel.timeline {
+ case .remoteLocal:
+ StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath, isRemote: true)
+ default:
+ StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
+ .environment(\.isHomeTimeline, timeline == .home)
}
}
- .safeAreaInset(edge: .top, spacing: 0) {
- if canFilterTimeline, !pinnedFilters.isEmpty {
- VStack(spacing: 0) {
- TimelineQuickAccessPills(pinnedFilters: $pinnedFilters, timeline: $timeline)
- .padding(.vertical, 8)
- .padding(.horizontal, .layoutPadding)
- .background(theme.primaryBackgroundColor.opacity(0.30))
- .background(Material.ultraThin)
- Divider()
+ .id(client.id)
+ .environment(\.defaultMinListRowHeight, 1)
+ .listStyle(.plain)
+ #if !os(visionOS)
+ .scrollContentBackground(.hidden)
+ .background(theme.primaryBackgroundColor)
+ #endif
+ .introspect(.list, on: .iOS(.v17, .v18)) { (collectionView: UICollectionView) in
+ DispatchQueue.main.async {
+ self.collectionView = collectionView
}
+ prefetcher.viewModel = viewModel
+ collectionView.isPrefetchingEnabled = true
+ collectionView.prefetchDataSource = prefetcher
}
+ if viewModel.timeline.supportNewestPagination {
+ TimelineUnreadStatusesView(observer: viewModel.pendingStatusesObserver)
}
- .if(canFilterTimeline && !pinnedFilters.isEmpty) { view in
- view.toolbarBackground(.hidden, for: .navigationBar)
- }
- .onChange(of: viewModel.scrollToIndex) { _, newValue in
- if let collectionView,
- let newValue,
- let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0),
- rows > newValue
- {
- collectionView.scrollToItem(at: .init(row: newValue, section: 0),
- at: .top,
- animated: viewModel.scrollToIndexAnimated)
- viewModel.scrollToIndexAnimated = false
- viewModel.scrollToIndex = nil
+ }
+ .safeAreaInset(edge: .top, spacing: 0) {
+ if canFilterTimeline, !pinnedFilters.isEmpty {
+ VStack(spacing: 0) {
+ TimelineQuickAccessPills(pinnedFilters: $pinnedFilters, timeline: $timeline)
+ .padding(.vertical, 8)
+ .padding(.horizontal, .layoutPadding)
+ .background(theme.primaryBackgroundColor.opacity(0.30))
+ .background(Material.ultraThin)
+ Divider()
}
}
- .onChange(of: scrollToTopSignal) {
- withAnimation {
- proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
- }
+ }
+ .if(canFilterTimeline && !pinnedFilters.isEmpty) { view in
+ view.toolbarBackground(.hidden, for: .navigationBar)
+ }
+ .onChange(of: viewModel.scrollToIndex) { _, newValue in
+ if let collectionView,
+ let newValue,
+ let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0),
+ rows > newValue
+ {
+ collectionView.scrollToItem(at: .init(row: newValue, section: 0),
+ at: .top,
+ animated: viewModel.scrollToIndexAnimated)
+ viewModel.scrollToIndexAnimated = false
+ viewModel.scrollToIndex = nil
}
}
.toolbar {
diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift
index 31d544b99..5d8d31d9b 100644
--- a/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift
+++ b/Packages/Timeline/Tests/TimelineTests/TimelineFilterTests.swift
@@ -1,25 +1,31 @@
import Models
import Network
+import Testing
+import Foundation
@testable import Timeline
-import XCTest
-final class TimelineFilterTests: XCTestCase {
- func testCodableHome() throws {
- XCTAssertTrue(try testCodableOn(filter: .home))
- XCTAssertTrue(try testCodableOn(filter: .local))
- XCTAssertTrue(try testCodableOn(filter: .federated))
- XCTAssertTrue(try testCodableOn(filter: .remoteLocal(server: "me.dm", filter: .local)))
- XCTAssertTrue(try testCodableOn(filter: .tagGroup(title: "test", tags: ["test"], symbolName: nil)))
- XCTAssertTrue(try testCodableOn(filter: .tagGroup(title: "test", tags: ["test"], symbolName: "test")))
- XCTAssertTrue(try testCodableOn(filter: .hashtag(tag: "test", accountId: nil)))
- XCTAssertTrue(try testCodableOn(filter: .list(list: .init(id: "test", title: "test"))))
+@Suite("Timeline Filter Tests")
+struct TimelineFilterTests {
+ @Test("All timeline filter can be decoded and encoded",
+ arguments: [TimelineFilter.home, TimelineFilter.local, TimelineFilter.federated,
+ TimelineFilter.remoteLocal(server: "me.dm", filter: .local),
+ TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: nil),
+ TimelineFilter.tagGroup(title: "test", tags: ["test"], symbolName: "test"),
+ TimelineFilter.hashtag(tag: "test", accountId: nil),
+ TimelineFilter.list(list: .init(id: "test", title: "test", repliesPolicy: .list, exclusive: true))])
+ func timelineCanEncodeAndDecode(filter: TimelineFilter) {
+ #expect(testCodableOn(filter: filter))
}
- private func testCodableOn(filter: TimelineFilter) throws -> Bool {
+ func testCodableOn(filter: TimelineFilter) -> Bool {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
- let data = try encoder.encode(filter)
- let newFilter = try decoder.decode(TimelineFilter.self, from: data)
+ guard let data = try? encoder.encode(filter) else {
+ return false
+ }
+ let newFilter = try? decoder.decode(TimelineFilter.self, from: data)
return newFilter == filter
+
}
+
}
diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift
index 310bd1f44..e0fbfe454 100644
--- a/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift
+++ b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift
@@ -2,62 +2,71 @@ import Models
import Network
@testable import Timeline
import XCTest
+import Testing
@MainActor
-final class TimelineViewModelTests: XCTestCase {
- var subject = TimelineViewModel()
-
- override func setUp() async throws {
- subject = TimelineViewModel()
+@Suite("Timeline View Model tests")
+struct Tests {
+ func makeSubject() -> TimelineViewModel {
+ let subject = TimelineViewModel()
let client = Client(server: "localhost")
subject.client = client
subject.timeline = .home
subject.isTimelineVisible = true
subject.timelineTask?.cancel()
+ return subject
}
-
- func testStreamEventInsertNewStatus() async throws {
+
+ @Test
+ func streamEventInsertNewStatus() async throws {
+ let subject = makeSubject()
let isEmpty = await subject.datasource.isEmpty
- XCTAssertTrue(isEmpty)
+ #expect(isEmpty)
await subject.datasource.append(.placeholder())
var count = await subject.datasource.count()
- XCTAssertTrue(count == 1)
+ #expect(count == 1)
await subject.handleEvent(event: StreamEventUpdate(status: .placeholder()))
count = await subject.datasource.count()
- XCTAssertTrue(count == 2)
+ #expect(count == 2)
}
-
- func testStreamEventInsertDuplicateStatus() async throws {
+
+ @Test
+ func streamEventInsertDuplicateStatus() async throws {
+ let subject = makeSubject()
let isEmpty = await subject.datasource.isEmpty
- XCTAssertTrue(isEmpty)
+ #expect(isEmpty)
let status = Status.placeholder()
await subject.datasource.append(status)
var count = await subject.datasource.count()
- XCTAssertTrue(count == 1)
+ #expect(count == 1)
await subject.handleEvent(event: StreamEventUpdate(status: status))
count = await subject.datasource.count()
- XCTAssertTrue(count == 1)
+ #expect(count == 1)
}
- func testStreamEventRemove() async throws {
+ @Test
+ func streamEventRemove() async throws {
+ let subject = makeSubject()
let isEmpty = await subject.datasource.isEmpty
- XCTAssertTrue(isEmpty)
+ #expect(isEmpty)
let status = Status.placeholder()
await subject.datasource.append(status)
var count = await subject.datasource.count()
- XCTAssertTrue(count == 1)
+ #expect(count == 1)
await subject.handleEvent(event: StreamEventDelete(status: status.id))
count = await subject.datasource.count()
- XCTAssertTrue(count == 0)
+ #expect(count == 0)
}
- func testStreamEventUpdateStatus() async throws {
+ @Test
+ func streamEventUpdateStatus() async throws {
+ let subject = makeSubject()
var status = Status.placeholder()
let isEmpty = await subject.datasource.isEmpty
- XCTAssertTrue(isEmpty)
+ #expect(isEmpty)
await subject.datasource.append(status)
var count = await subject.datasource.count()
- XCTAssertTrue(count == 1)
+ #expect(count == 1)
status = .init(id: status.id,
content: .init(stringValue: "test"),
account: status.account,
@@ -88,7 +97,7 @@ final class TimelineViewModelTests: XCTestCase {
await subject.handleEvent(event: StreamEventStatusUpdate(status: status))
let statuses = await subject.datasource.get()
count = await subject.datasource.count()
- XCTAssertTrue(count == 1)
- XCTAssertTrue(statuses.first?.content.asRawText == "test")
+ #expect(count == 1)
+ #expect(statuses.first?.content.asRawText == "test")
}
}