This commit is contained in:
Thomas Ricouard 2024-08-01 08:58:54 +02:00
parent 2a3da72239
commit a72f290038
38 changed files with 209 additions and 207 deletions

View file

@ -101,12 +101,12 @@ struct SideBarView<Content: View>: View {
} label: { } label: {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
if userPreferences.isSidebarExpanded { if userPreferences.isSidebarExpanded {
AppAccountView(viewModel: .init(appAccount: account, AppAccountView(viewModel: .init(appAccount: account,
isCompact: false, isCompact: false,
isInSettings: false), isInSettings: false),
isParentPresented: .constant(false)) isParentPresented: .constant(false))
} else { } else {
AppAccountView(viewModel: .init(appAccount: account, AppAccountView(viewModel: .init(appAccount: account,
isCompact: true, isCompact: true,
isInSettings: false), isInSettings: false),
isParentPresented: .constant(false)) isParentPresented: .constant(false))

View file

@ -1,4 +1,5 @@
import AppAccount import AppAccount
import AuthenticationServices
import Combine import Combine
import DesignSystem import DesignSystem
import Env import Env
@ -7,7 +8,6 @@ import Network
import NukeUI import NukeUI
import SafariServices import SafariServices
import SwiftUI import SwiftUI
import AuthenticationServices
@MainActor @MainActor
struct AddAccountView: View { struct AddAccountView: View {

View file

@ -31,9 +31,9 @@ struct SettingsTabs: View {
@Binding var popToRootTab: Tab @Binding var popToRootTab: Tab
let isModal: Bool let isModal: Bool
@State private var startingPoint: SettingsStartingPoint? = nil @State private var startingPoint: SettingsStartingPoint? = nil
var body: some View { var body: some View {
NavigationStack(path: $routerPath.path) { NavigationStack(path: $routerPath.path) {
Form { Form {
@ -72,24 +72,24 @@ struct SettingsTabs: View {
} }
.navigationDestination(item: $startingPoint) { targetView in .navigationDestination(item: $startingPoint) { targetView in
switch targetView { switch targetView {
case .display: case .display:
DisplaySettingsView() DisplaySettingsView()
case .haptic: case .haptic:
HapticSettingsView() HapticSettingsView()
case .remoteTimelines: case .remoteTimelines:
RemoteTimelinesSettingView() RemoteTimelinesSettingView()
case .tagGroups: case .tagGroups:
TagsGroupSettingView() TagsGroupSettingView()
case .recentTags: case .recentTags:
RecenTagsSettingView() RecenTagsSettingView()
case .content: case .content:
ContentSettingsView() ContentSettingsView()
case .swipeActions: case .swipeActions:
SwipeActionsSettingsView() SwipeActionsSettingsView()
case .tabAndSidebarEntries: case .tabAndSidebarEntries:
EmptyView() EmptyView()
case .translation: case .translation:
TranslationSettingsView() TranslationSettingsView()
} }
} }
} }

View file

@ -22,24 +22,24 @@ public struct ListEntity: Identifiable, AppEntity {
public struct DefaultListEntityQuery: EntityQuery { public struct DefaultListEntityQuery: EntityQuery {
public init() {} public init() {}
@IntentParameterDependency<ListsWidgetConfiguration>( @IntentParameterDependency<ListsWidgetConfiguration>(
\.$account \.$account
) )
var account var account
public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] { public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] {
await fetchLists().map{ .init(list: $0 )} await fetchLists().map { .init(list: $0) }
} }
public func suggestedEntities() async throws -> [ListEntity] { public func suggestedEntities() async throws -> [ListEntity] {
await fetchLists().map{ .init(list: $0 )} await fetchLists().map { .init(list: $0) }
} }
public func defaultResult() async -> ListEntity? { public func defaultResult() async -> ListEntity? {
nil nil
} }
private func fetchLists() async -> [Models.List] { private func fetchLists() async -> [Models.List] {
guard let account = account?.account.account else { guard let account = account?.account.account else {
return [] return []

View file

@ -10,18 +10,18 @@ struct AccountWidgetProvider: AppIntentTimelineProvider {
.init(date: Date(), account: .placeholder(), avatar: nil) .init(date: Date(), account: .placeholder(), avatar: nil)
} }
func snapshot(for configuration: AccountWidgetConfiguration, in context: Context) async -> AccountWidgetEntry { func snapshot(for configuration: AccountWidgetConfiguration, in _: Context) async -> AccountWidgetEntry {
let account = await fetchAccount(configuration: configuration) let account = await fetchAccount(configuration: configuration)
return .init(date: Date(), account: account, avatar: nil) return .init(date: Date(), account: account, avatar: nil)
} }
func timeline(for configuration: AccountWidgetConfiguration, in context: Context) async -> Timeline<AccountWidgetEntry> { func timeline(for configuration: AccountWidgetConfiguration, in _: Context) async -> Timeline<AccountWidgetEntry> {
let account = await fetchAccount(configuration: configuration) let account = await fetchAccount(configuration: configuration)
let images = try? await loadImages(urls: [account.avatar]) let images = try? await loadImages(urls: [account.avatar])
return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)], return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)],
policy: .atEnd) policy: .atEnd)
} }
private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account { private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account {
let client = Client(server: configuration.account.account.server, let client = Client(server: configuration.account.account.server,
oauthToken: configuration.account.account.oauthToken) oauthToken: configuration.account.account.oauthToken)

View file

@ -11,7 +11,6 @@ struct AccountWidgetView: View {
@Environment(\.widgetFamily) var family @Environment(\.widgetFamily) var family
@Environment(\.redactionReasons) var redacted @Environment(\.redactionReasons) var redacted
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 4) { VStack(alignment: .center, spacing: 4) {
if let avatar = entry.avatar { if let avatar = entry.avatar {

View file

@ -138,13 +138,13 @@ public struct AccountDetailContextMenu: View {
} }
#if canImport(_Translation_SwiftUI) #if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *) { if #available(iOS 17.4, *) {
Button { Button {
showTranslateView = true showTranslateView = true
} label: { } label: {
Label("status.action.translate", systemImage: "captions.bubble") Label("status.action.translate", systemImage: "captions.bubble")
}
} }
}
#endif #endif
if viewModel.relationship?.following == true { if viewModel.relationship?.following == true {

View file

@ -85,14 +85,14 @@ public struct AccountDetailView: View {
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
.onTapGesture { .onTapGesture {
if let account = viewModel.account { if let account = viewModel.account {
routerPath.navigate(to: .accountMediaGridView(account: account, routerPath.navigate(to: .accountMediaGridView(account: account,
initialMediaStatuses: viewModel.statusesMedias)) initialMediaStatuses: viewModel.statusesMedias))
}
} }
}
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
} }
StatusesListView(fetcher: viewModel, StatusesListView(fetcher: viewModel,
@ -302,7 +302,7 @@ public struct AccountDetailView: View {
} }
Menu { Menu {
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
showTranslateView: $showTranslateView, showTranslateView: $showTranslateView,
viewModel: viewModel) viewModel: viewModel)

View file

@ -85,7 +85,7 @@ import SwiftUI
private(set) var statuses: [Status] = [] private(set) var statuses: [Status] = []
var statusesMedias: [MediaStatus] { var statusesMedias: [MediaStatus] {
statuses.filter{ !$0.mediaAttachments.isEmpty }.flatMap{ $0.asMediaStatus} statuses.filter { !$0.mediaAttachments.isEmpty }.flatMap { $0.asMediaStatus }
} }
var boosts: [Status] = [] var boosts: [Status] = []

View file

@ -113,7 +113,7 @@ public struct AccountsListRow: View {
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText) .addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText)
#endif #endif
.contextMenu { .contextMenu {
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
showTranslateView: $showTranslateView, showTranslateView: $showTranslateView,
viewModel: .init(account: viewModel.account)) viewModel: .init(account: viewModel.account))
} preview: { } preview: {

View file

@ -128,7 +128,7 @@ public struct AccountsListView: View {
PlaceholderView(iconName: "person.icloud", PlaceholderView(iconName: "person.icloud",
title: "No accounts found", title: "No accounts found",
message: "This list of accounts is empty") message: "This list of accounts is empty")
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
} else { } else {
ForEach(accounts) { account in ForEach(accounts) { account in
if let relationship = relationships.first(where: { $0.id == account.id }) { if let relationship = relationships.first(where: { $0.id == account.id }) {
@ -139,7 +139,7 @@ public struct AccountsListView: View {
} }
} }
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
switch nextPageState { switch nextPageState {

View file

@ -99,21 +99,21 @@ public struct EditAccountView: View {
viewModel.isPhotoPickerPresented = true viewModel.isPhotoPickerPresented = true
} }
if viewModel.avatar != nil || viewModel.header != nil { if viewModel.avatar != nil || viewModel.header != nil {
Divider() Divider()
} }
if viewModel.avatar != nil { if viewModel.avatar != nil {
Button("account.edit.avatar.delete", role: .destructive) { Button("account.edit.avatar.delete", role: .destructive) {
Task { Task {
await viewModel.deleteAvatar() await viewModel.deleteAvatar()
}
} }
}
} }
if viewModel.header != nil { if viewModel.header != nil {
Button("account.edit.header.delete", role: .destructive) { Button("account.edit.header.delete", role: .destructive) {
Task { Task {
await viewModel.deleteHeader() await viewModel.deleteHeader()
}
} }
}
} }
} label: { } label: {
Image(systemName: "photo.badge.plus") Image(systemName: "photo.badge.plus")

View file

@ -51,19 +51,19 @@ import SwiftUI
didSet { didSet {
if let item = mediaPickers.first { if let item = mediaPickers.first {
Task { Task {
if isChangingAvatar { if isChangingAvatar {
if let data = await getItemImageData(item: item, for: .avatar) { if let data = await getItemImageData(item: item, for: .avatar) {
_ = await uploadAvatar(data: data) _ = await uploadAvatar(data: data)
}
isChangingAvatar = false
} else if isChangingHeader {
if let data = await getItemImageData(item: item, for: .header) {
_ = await uploadHeader(data: data)
}
isChangingHeader = false
} }
await fetchAccount() isChangingAvatar = false
mediaPickers = [] } else if isChangingHeader {
if let data = await getItemImageData(item: item, for: .header) {
_ = await uploadHeader(data: data)
}
isChangingHeader = false
}
await fetchAccount()
mediaPickers = []
} }
} }
} }
@ -186,26 +186,26 @@ import SwiftUI
} }
extension EditAccountViewModel { extension EditAccountViewModel {
private enum ItemType { private enum ItemType {
case avatar case avatar
case header case header
var maxHeight: CGFloat { var maxHeight: CGFloat {
switch self { switch self {
case .avatar: case .avatar:
400 400
case .header: case .header:
500 500
} }
}
var maxWidth: CGFloat {
switch self {
case .avatar:
400
case .header:
1500
}
}
} }
var maxWidth: CGFloat {
switch self {
case .avatar:
400
case .header:
1500
}
}
}
} }

View file

@ -1,10 +1,10 @@
import SwiftUI
import DesignSystem import DesignSystem
import NukeUI
import Env import Env
import MediaUI import MediaUI
import Models import Models
import Network import Network
import NukeUI
import SwiftUI
@MainActor @MainActor
public struct AccountDetailMediaGridView: View { public struct AccountDetailMediaGridView: View {
@ -12,21 +12,22 @@ public struct AccountDetailMediaGridView: View {
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Environment(Client.self) private var client @Environment(Client.self) private var client
@Environment(QuickLook.self) private var quickLook @Environment(QuickLook.self) private var quickLook
let account: Account let account: Account
@State var mediaStatuses: [MediaStatus] @State var mediaStatuses: [MediaStatus]
public init(account: Account, initialMediaStatuses: [MediaStatus]) { public init(account: Account, initialMediaStatuses: [MediaStatus]) {
self.account = account self.account = account
self.mediaStatuses = initialMediaStatuses mediaStatuses = initialMediaStatuses
} }
public var body: some View { public var body: some View {
ScrollView(.vertical) { ScrollView(.vertical) {
LazyVGrid(columns: [.init(.flexible(minimum: 100), spacing: 4), LazyVGrid(columns: [.init(.flexible(minimum: 100), spacing: 4),
.init(.flexible(minimum: 100), spacing: 4), .init(.flexible(minimum: 100), spacing: 4),
.init(.flexible(minimum: 100), spacing: 4)], .init(.flexible(minimum: 100), spacing: 4)],
spacing: 4) { spacing: 4)
{
ForEach(mediaStatuses) { status in ForEach(mediaStatuses) { status in
GeometryReader { proxy in GeometryReader { proxy in
if let url = status.attachment.url { if let url = status.attachment.url {
@ -35,7 +36,7 @@ public struct AccountDetailMediaGridView: View {
case .image: case .image:
LazyImage(url: url, transaction: Transaction(animation: .easeIn)) { state in LazyImage(url: url, transaction: Transaction(animation: .easeIn)) { state in
if let image = state.image { if let image = state.image {
image image
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.width) .frame(width: proxy.size.width, height: proxy.size.width)
@ -84,7 +85,7 @@ public struct AccountDetailMediaGridView: View {
.clipped() .clipped()
.aspectRatio(1, contentMode: .fit) .aspectRatio(1, contentMode: .fit)
} }
VStack { VStack {
Spacer() Spacer()
NextPageView { NextPageView {
@ -96,21 +97,21 @@ public struct AccountDetailMediaGridView: View {
} }
.navigationTitle(account.displayName ?? "") .navigationTitle(account.displayName ?? "")
#if !os(visionOS) #if !os(visionOS)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor) .background(theme.primaryBackgroundColor)
#endif #endif
} }
private func fetchNextPage() async throws { private func fetchNextPage() async throws {
let client = client let client = client
let newStatuses: [Status] = let newStatuses: [Status] =
try await client.get(endpoint: Accounts.statuses(id: account.id, try await client.get(endpoint: Accounts.statuses(id: account.id,
sinceId: mediaStatuses.last?.id, sinceId: mediaStatuses.last?.id,
tag: nil, tag: nil,
onlyMedia: true, onlyMedia: true,
excludeReplies: true, excludeReplies: true,
excludeReblogs: true, excludeReblogs: true,
pinned: nil)) pinned: nil))
mediaStatuses.append(contentsOf: newStatuses.flatMap{ $0.asMediaStatus }) mediaStatuses.append(contentsOf: newStatuses.flatMap { $0.asMediaStatus })
} }
} }

View file

@ -284,7 +284,7 @@ public final class Theme {
themeStorage.chosenFontData = chosenFontData themeStorage.chosenFontData = chosenFontData
} }
} }
public var showContentGradient: Bool { public var showContentGradient: Bool {
didSet { didSet {
themeStorage.showContentGradient = showContentGradient themeStorage.showContentGradient = showContentGradient

View file

@ -38,7 +38,7 @@ import Observation
public var isNotificationsFilterSupported: Bool { public var isNotificationsFilterSupported: Bool {
version >= 4.3 version >= 4.3
} }
public var isLinkTimelineSupported: Bool { public var isLinkTimelineSupported: Bool {
version >= 4.3 version >= 4.3
} }

View file

@ -3,8 +3,8 @@ import SwiftUI
#if canImport(_Translation_SwiftUI) #if canImport(_Translation_SwiftUI)
import Translation import Translation
extension View { public extension View {
public func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View { func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View {
if #available(iOS 17.4, *) { if #available(iOS 17.4, *) {
return self.translationPresentation(isPresented: isPresented, text: text) return self.translationPresentation(isPresented: isPresented, text: text)
} else { } else {

View file

@ -137,7 +137,7 @@ public enum SettingsStartingPoint {
public var path: [RouterDestination] = [] public var path: [RouterDestination] = []
public var presentedSheet: SheetDestination? public var presentedSheet: SheetDestination?
public static var settingsStartingPoint: SettingsStartingPoint? = nil public static var settingsStartingPoint: SettingsStartingPoint?
public init() {} public init() {}

View file

@ -354,7 +354,7 @@ public struct ExploreView: View {
viewModel.scrollToTopVisible = false viewModel.scrollToTopVisible = false
} }
} }
private func makeNextPageView(for type: Search.EntityType) -> some View { private func makeNextPageView(for type: Search.EntityType) -> some View {
NextPageView { NextPageView {
await viewModel.fetchNextPage(of: type) await viewModel.fetchNextPage(of: type)

View file

@ -115,7 +115,7 @@ import SwiftUI
isSearching = false isSearching = false
} }
} }
func fetchNextPage(of type: Search.EntityType) async { func fetchNextPage(of type: Search.EntityType) async {
guard let client, !searchQuery.isEmpty, guard let client, !searchQuery.isEmpty,
let results = results[searchQuery] else { return } let results = results[searchQuery] else { return }
@ -128,18 +128,18 @@ import SwiftUI
case .statuses: case .statuses:
results.statuses.count results.statuses.count
} }
var newPageResults: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, var newPageResults: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery,
type: type, type: type,
offset: offset, offset: offset,
following: nil), following: nil),
forceVersion: .v2) forceVersion: .v2)
if type == .accounts { if type == .accounts {
let relationships: [Relationship] = let relationships: [Relationship] =
try await client.get(endpoint: Accounts.relationships(ids: newPageResults.accounts.map(\.id))) try await client.get(endpoint: Accounts.relationships(ids: newPageResults.accounts.map(\.id)))
newPageResults.relationships = relationships newPageResults.relationships = relationships
} }
switch type { switch type {
case .accounts: case .accounts:
self.results[searchQuery]?.accounts.append(contentsOf: newPageResults.accounts) self.results[searchQuery]?.accounts.append(contentsOf: newPageResults.accounts)
@ -149,8 +149,6 @@ import SwiftUI
case .statuses: case .statuses:
self.results[searchQuery]?.statuses.append(contentsOf: newPageResults.statuses) self.results[searchQuery]?.statuses.append(contentsOf: newPageResults.statuses)
} }
} catch { } catch {}
}
} }
} }

View file

@ -8,7 +8,7 @@ public struct MediaUIShareLink: View, @unchecked Sendable {
self.url = url self.url = url
self.type = type self.type = type
} }
public var body: some View { public var body: some View {
if type == .image { if type == .image {
let transferable = MediaUIImageTransferable(url: url) let transferable = MediaUIImageTransferable(url: url)

View file

@ -8,7 +8,7 @@ public struct MediaUIImageTransferable: Codable, Transferable {
public init(url: URL) { public init(url: URL) {
self.url = url self.url = url
} }
public func fetchData() async -> Data { public func fetchData() async -> Data {
do { do {
return try await URLSession.shared.data(from: url).0 return try await URLSession.shared.data(from: url).0

View file

@ -175,9 +175,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
return return
} else if node.nodeName() == "#text" { } else if node.nodeName() == "#text" {
var txt = node.description var txt = node.description
txt = (try? Entities.unescape(txt)) ?? txt txt = (try? Entities.unescape(txt)) ?? txt
if let underscore_regex, let main_regex { if let underscore_regex, let main_regex {
// This is the markdown escaper // This is the markdown escaper
txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1") txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1")

View file

@ -4,11 +4,12 @@ public struct Card: Codable, Identifiable, Equatable, Hashable {
public var id: String { public var id: String {
url url
} }
public struct CardAuthor: Codable, Sendable, Identifiable, Equatable, Hashable { public struct CardAuthor: Codable, Sendable, Identifiable, Equatable, Hashable {
public var id: String { public var id: String {
url url
} }
public let name: String public let name: String
public let url: String public let url: String
public let account: Account? public let account: Account?

View file

@ -4,10 +4,10 @@ public struct MediaStatus: Sendable, Identifiable, Hashable {
public var id: String { public var id: String {
attachment.id attachment.id
} }
public let status: Status public let status: Status
public let attachment: MediaAttachment public let attachment: MediaAttachment
public init(status: Status, attachment: MediaAttachment) { public init(status: Status, attachment: MediaAttachment) {
self.status = status self.status = status
self.attachment = attachment self.attachment = attachment

View file

@ -77,9 +77,9 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
public var isHidden: Bool { public var isHidden: Bool {
filtered?.first?.filter.filterAction == .hide filtered?.first?.filter.filterAction == .hide
} }
public var asMediaStatus: [MediaStatus] { public var asMediaStatus: [MediaStatus] {
mediaAttachments.map{ .init(status: self, attachment: $0)} mediaAttachments.map { .init(status: self, attachment: $0) }
} }
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) { public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {

View file

@ -4,7 +4,6 @@ public enum Profile: Endpoint {
case deleteAvatar case deleteAvatar
case deleteHeader case deleteHeader
public func path() -> String { public func path() -> String {
switch self { switch self {
case .deleteAvatar: case .deleteAvatar:

View file

@ -4,7 +4,7 @@ public enum Search: Endpoint {
public enum EntityType: String, Sendable { public enum EntityType: String, Sendable {
case accounts, hashtags, statuses case accounts, hashtags, statuses
} }
case search(query: String, type: EntityType?, offset: Int?, following: Bool?) case search(query: String, type: EntityType?, offset: Int?, following: Bool?)
case accountsSearch(query: String, type: EntityType?, offset: Int?, following: Bool?) case accountsSearch(query: String, type: EntityType?, offset: Int?, following: Bool?)

View file

@ -58,10 +58,10 @@ public extension StatusEditor {
} }
public func compressImageForUpload( public func compressImageForUpload(
_ image: UIImage, _ image: UIImage,
maxSize: Int = 10 * 1024 * 1024, maxSize: Int = 10 * 1024 * 1024,
maxHeight: Double = 5000, maxHeight: Double = 5000,
maxWidth: Double = 5000 maxWidth: Double = 5000
) async throws -> Data { ) async throws -> Data {
var image = image var image = image

View file

@ -38,9 +38,9 @@ extension StatusEditor {
generateButton generateButton
} }
#if canImport(_Translation_SwiftUI) #if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *), !imageDescription.isEmpty { if #available(iOS 17.4, *), !imageDescription.isEmpty {
translateButton translateButton
} }
#endif #endif
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)

View file

@ -194,7 +194,7 @@ extension StatusEditor {
} }
} }
private func makeErrorView(error: ServerError) -> some View { private func makeErrorView(error _: ServerError) -> some View {
ZStack { ZStack {
placeholderView placeholderView
Text("status.editor.error.upload") Text("status.editor.error.upload")

View file

@ -3,12 +3,12 @@ import SwiftUI
public struct StatusRowExternalView: View { public struct StatusRowExternalView: View {
@State private var viewModel: StatusRowViewModel @State private var viewModel: StatusRowViewModel
private let context: StatusRowView.Context private let context: StatusRowView.Context
public init(viewModel: StatusRowViewModel, context: StatusRowView.Context = .timeline) { public init(viewModel: StatusRowViewModel, context: StatusRowView.Context = .timeline) {
_viewModel = .init(initialValue: viewModel) _viewModel = .init(initialValue: viewModel)
self.context = context self.context = context
} }
public var body: some View { public var body: some View {
StatusRowView(viewModel: viewModel, context: context) StatusRowView(viewModel: viewModel, context: context)
} }

View file

@ -16,7 +16,7 @@ public struct StatusRowView: View {
@Environment(\.isStatusFocused) private var isFocused @Environment(\.isStatusFocused) private var isFocused
@Environment(\.indentationLevel) private var indentationLevel @Environment(\.indentationLevel) private var indentationLevel
@Environment(\.isHomeTimeline) private var isHomeTimeline @Environment(\.isHomeTimeline) private var isHomeTimeline
@Environment(RouterPath.self) private var routerPath: RouterPath @Environment(RouterPath.self) private var routerPath: RouterPath
@Environment(QuickLook.self) private var quickLook @Environment(QuickLook.self) private var quickLook
@ -27,7 +27,7 @@ public struct StatusRowView: View {
@State private var isBlockConfirmationPresented = false @State private var isBlockConfirmationPresented = false
public enum Context { case timeline, detail } public enum Context { case timeline, detail }
@State public var viewModel: StatusRowViewModel @State public var viewModel: StatusRowViewModel
public let context: Context public let context: Context

View file

@ -18,7 +18,7 @@ import SwiftUI
let client: Client let client: Client
let routerPath: RouterPath let routerPath: RouterPath
let userFollowedTag: HTMLString.Link? let userFollowedTag: HTMLString.Link?
private let theme = Theme.shared private let theme = Theme.shared
@ -118,7 +118,7 @@ import SwiftUI
backgroundColor backgroundColor
} }
} }
@ViewBuilder @ViewBuilder
var homeBackgroundColor: some View { var homeBackgroundColor: some View {
if status.visibility == .direct { if status.visibility == .direct {
@ -146,7 +146,7 @@ import SwiftUI
theme.primaryBackgroundColor theme.primaryBackgroundColor
} }
} }
func makeDecorativeGradient(startColor: Color, endColor: Color) -> some View { func makeDecorativeGradient(startColor: Color, endColor: Color) -> some View {
LinearGradient(stops: [ LinearGradient(stops: [
.init(color: startColor.opacity(0.2), location: 0.03), .init(color: startColor.opacity(0.2), location: 0.03),
@ -155,8 +155,8 @@ import SwiftUI
.init(color: startColor.opacity(0.02), location: 0.15), .init(color: startColor.opacity(0.02), location: 0.15),
.init(color: endColor, location: 0.25), .init(color: endColor, location: 0.25),
], ],
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing) endPoint: .bottomTrailing)
} }
public init(status: Status, public init(status: Status,
@ -191,7 +191,7 @@ import SwiftUI
} else { } else {
userMentionned = false userMentionned = false
} }
userFollowedTag = finalStatus.content.links.first(where: { link in userFollowedTag = finalStatus.content.links.first(where: { link in
link.type == .hashtag && CurrentAccount.shared.tags.contains(where: { $0.name.lowercased() == link.title.lowercased() }) link.type == .hashtag && CurrentAccount.shared.tags.contains(where: { $0.name.lowercased() == link.title.lowercased() })
}) })
@ -366,29 +366,31 @@ import SwiftUI
if preferredTranslationType != .useDeepl { if preferredTranslationType != .useDeepl {
await translateWithInstance(userLang: userLang) await translateWithInstance(userLang: userLang)
if translation == nil { if translation == nil {
await translateWithDeepL(userLang: userLang) await translateWithDeepL(userLang: userLang)
} }
} else { } else {
await translateWithDeepL(userLang: userLang) await translateWithDeepL(userLang: userLang)
if translation == nil { if translation == nil {
await translateWithInstance(userLang: userLang) await translateWithInstance(userLang: userLang)
} }
} }
var hasShown = false var hasShown = false
#if canImport(_Translation_SwiftUI) #if canImport(_Translation_SwiftUI)
if translation == nil, if translation == nil,
#available(iOS 17.4, *) { #available(iOS 17.4, *)
showAppleTranslation = true {
hasShown = true showAppleTranslation = true
} hasShown = true
#endif }
#endif
if !hasShown, if !hasShown,
translation == nil { translation == nil
{
if preferredTranslationType == .useDeepl { if preferredTranslationType == .useDeepl {
deeplTranslationError = true deeplTranslationError = true
} else { } else {
@ -410,7 +412,7 @@ import SwiftUI
isLoadingTranslation = false isLoadingTranslation = false
} }
} }
func translateWithInstance(userLang: String) async { func translateWithInstance(userLang: String) async {
withAnimation { withAnimation {
isLoadingTranslation = true isLoadingTranslation = true

View file

@ -186,7 +186,7 @@ public struct StatusRowCardView: View {
} }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
Spacer() Spacer()
Button { Button {
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
@ -248,7 +248,7 @@ public struct StatusRowCardView: View {
}.padding(16) }.padding(16)
} }
} }
@ViewBuilder @ViewBuilder
private func moreFromAccountView(_ account: Account, divider: Bool = true) -> some View { private func moreFromAccountView(_ account: Account, divider: Bool = true) -> some View {
if divider { if divider {

View file

@ -95,9 +95,9 @@ struct StatusRowHeaderView: View {
private var dateView: some View { private var dateView: some View {
Text("\(Image(systemName: viewModel.finalStatus.visibility.iconName))\(viewModel.finalStatus.createdAt.relativeFormatted)") Text("\(Image(systemName: viewModel.finalStatus.visibility.iconName))\(viewModel.finalStatus.createdAt.relativeFormatted)")
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.font(.scaledFootnote) .font(.scaledFootnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(1) .lineLimit(1)
} }
} }

View file

@ -280,32 +280,32 @@ struct AltTextButton: View {
.padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7)) .padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7))
.background(.thinMaterial) .background(.thinMaterial)
#if canImport(_Translation_SwiftUI) #if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $isDisplayingTranslation, text: text) .addTranslateView(isPresented: $isDisplayingTranslation, text: text)
#endif #endif
#if os(visionOS) #if os(visionOS)
.clipShape(Capsule()) .clipShape(Capsule())
#endif #endif
.cornerRadius(4) .cornerRadius(4)
.padding(theme.statusDisplayStyle == .compact ? 0 : 10) .padding(theme.statusDisplayStyle == .compact ? 0 : 10)
.alert( .alert(
"status.editor.media.image-description", "status.editor.media.image-description",
isPresented: $isDisplayingAlert isPresented: $isDisplayingAlert
) { ) {
Button("alert.button.ok", action: {}) Button("alert.button.ok", action: {})
Button("status.action.copy-text", action: { UIPasteboard.general.string = text }) Button("status.action.copy-text", action: { UIPasteboard.general.string = text })
#if canImport(_Translation_SwiftUI) #if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *) { if #available(iOS 17.4, *) {
Button("status.action.translate", action: { isDisplayingTranslation = true }) Button("status.action.translate", action: { isDisplayingTranslation = true })
} }
#endif #endif
} message: { } message: {
Text(text) Text(text)
} }
.frame( .frame(
maxWidth: .infinity, maxWidth: .infinity,
maxHeight: .infinity, maxHeight: .infinity,
alignment: .bottomTrailing alignment: .bottomTrailing
) )
} }
} }
} }

View file

@ -307,25 +307,26 @@ extension TimelineViewModel: StatusesFetcher {
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws { private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
canStreamEvents = false canStreamEvents = false
let initialTimeline = timeline let initialTimeline = timeline
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client) let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
guard !newStatuses.isEmpty, guard !newStatuses.isEmpty,
isTimelineVisible, isTimelineVisible,
!Task.isCancelled, !Task.isCancelled,
initialTimeline == timeline else { initialTimeline == timeline
else {
canStreamEvents = true canStreamEvents = true
return return
} }
await updateTimelineWithNewStatuses(newStatuses) await updateTimelineWithNewStatuses(newStatuses)
if !Task.isCancelled, let latest = await datasource.get().first { if !Task.isCancelled, let latest = await datasource.get().first {
pendingStatusesObserver.isLoadingNewStatuses = true pendingStatusesObserver.isLoadingNewStatuses = true
try await fetchNewPagesFrom(latestStatus: latest.id, client: client) try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
} }
} }
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] { private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] {
var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5) var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5)
let ids = await datasource.get().map(\.id) let ids = await datasource.get().map(\.id)
@ -335,38 +336,39 @@ extension TimelineViewModel: StatusesFetcher {
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
return newStatuses return newStatuses
} }
private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async { private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
let topStatus = await datasource.getFiltered().first let topStatus = await datasource.getFiltered().first
await datasource.insert(contentOf: newStatuses, at: 0) await datasource.insert(contentOf: newStatuses, at: 0)
await cache() await cache()
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0) pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
let statuses = await datasource.getFiltered() let statuses = await datasource.getFiltered()
let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage
if let topStatus = topStatus, if let topStatus = topStatus,
visibileStatuses.contains(where: { $0.id == topStatus.id }), visibileStatuses.contains(where: { $0.id == topStatus.id }),
scrollToTopVisible { scrollToTopVisible
{
updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState) updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState)
} else { } else {
updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState) updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState)
} }
} }
// Refresh the timeline while keeping the scroll position to the top status. // Refresh the timeline while keeping the scroll position to the top status.
private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) { private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
pendingStatusesObserver.disableUpdate = true pendingStatusesObserver.disableUpdate = true
statusesState = .display(statuses: statuses, nextPageState: nextPageState) statusesState = .display(statuses: statuses, nextPageState: nextPageState)
scrollToIndexAnimated = false scrollToIndexAnimated = false
scrollToIndex = newStatuses.count + 1 scrollToIndex = newStatuses.count + 1
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.pendingStatusesObserver.disableUpdate = false self?.pendingStatusesObserver.disableUpdate = false
self?.canStreamEvents = true self?.canStreamEvents = true
} }
} }
// Refresh the timeline while keeping the user current position. // Refresh the timeline while keeping the user current position.
// It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top. // It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top.
private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) { private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) {
@ -381,18 +383,18 @@ extension TimelineViewModel: StatusesFetcher {
var allStatuses: [Status] = [] var allStatuses: [Status] = []
var latestMinId = minId var latestMinId = minId
do { do {
for _ in 1...maxPages { for _ in 1 ... maxPages {
if Task.isCancelled { break } if Task.isCancelled { break }
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint( let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
sinceId: nil, sinceId: nil,
maxId: nil, maxId: nil,
minId: latestMinId, minId: latestMinId,
offset: nil offset: nil
)) ))
if newStatuses.isEmpty { break } if newStatuses.isEmpty { break }
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
allStatuses.insert(contentsOf: newStatuses, at: 0) allStatuses.insert(contentsOf: newStatuses, at: 0)
latestMinId = newStatuses.first?.id ?? latestMinId latestMinId = newStatuses.first?.id ?? latestMinId
@ -400,7 +402,7 @@ extension TimelineViewModel: StatusesFetcher {
} catch { } catch {
return allStatuses return allStatuses
} }
return allStatuses return allStatuses
} }