mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-06-03 05:49:25 +00:00
Merge remote-tracking branch 'upstream/main' into zh-Hant-localization
This commit is contained in:
commit
45878a4d91
|
@ -940,7 +940,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -975,7 +975,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1011,7 +1011,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1045,7 +1045,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1225,7 +1225,7 @@
|
|||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
|
@ -1279,7 +1279,7 @@
|
|||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
|
@ -1314,7 +1314,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1349,7 +1349,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.10.32;
|
||||
MARKETING_VERSION = 1.10.33;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
|
|
@ -68,8 +68,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke",
|
||||
"state" : {
|
||||
"revision" : "15fde63470d782c897816a74bdd516a907e33147",
|
||||
"version" : "12.3.0"
|
||||
"revision" : "8ecbfc886da39bccb01c34abef5f2ff4073ad633",
|
||||
"version" : "12.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -140,6 +140,12 @@ extension View {
|
|||
.presentationDetents([.medium])
|
||||
.presentationBackground(.thinMaterial)
|
||||
.withEnvironments()
|
||||
case .accountEditInfo:
|
||||
EditAccountView()
|
||||
.withEnvironments()
|
||||
case .accountFiltersList:
|
||||
FiltersListView()
|
||||
.withEnvironments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct NavigationSheet<Content: View>: View {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct NavigationTab<Content: View>: View {
|
||||
|
|
|
@ -60,9 +60,9 @@ struct NotificationsTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTab, { _, newValue in
|
||||
.onChange(of: selectedTab) { _, _ in
|
||||
clearNotifications()
|
||||
})
|
||||
}
|
||||
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
||||
if let newValue, let type = newValue.notification.supportedType {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
|
|
|
@ -17,9 +17,8 @@ struct AccountSettingsView: View {
|
|||
@Environment(Theme.self) private var theme
|
||||
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
@State private var isEditingAccount: Bool = false
|
||||
@State private var isEditingFilters: Bool = false
|
||||
@State private var cachedPostsCount: Int = 0
|
||||
@State private var timelineCache = TimelineCache()
|
||||
|
||||
|
@ -30,7 +29,7 @@ struct AccountSettingsView: View {
|
|||
Form {
|
||||
Section {
|
||||
Button {
|
||||
isEditingAccount = true
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
} label: {
|
||||
Label("account.action.edit-info", systemImage: "pencil")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
@ -40,7 +39,7 @@ struct AccountSettingsView: View {
|
|||
|
||||
if currentInstance.isFiltersSupported {
|
||||
Button {
|
||||
isEditingFilters = true
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
} label: {
|
||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
@ -96,12 +95,6 @@ struct AccountSettingsView: View {
|
|||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.sheet(isPresented: $isEditingAccount, content: {
|
||||
EditAccountView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingFilters, content: {
|
||||
FiltersListView()
|
||||
})
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
|
|
|
@ -5,8 +5,8 @@ import Models
|
|||
import Network
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import Timeline
|
||||
import UserNotifications
|
||||
|
||||
@MainActor
|
||||
struct ContentSettingsView: View {
|
||||
|
|
|
@ -7,6 +7,7 @@ import Observation
|
|||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable class DisplaySettingsLocalValues {
|
||||
var tintColor = Theme.shared.tintColor
|
||||
var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import SwiftUI
|
||||
import SwiftData
|
||||
import Models
|
||||
import Env
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
struct RecenTagsSettingView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import SwiftUI
|
||||
import SwiftData
|
||||
import Models
|
||||
import Env
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
struct RemoteTimelinesSettingView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
|
|
|
@ -2,6 +2,7 @@ import DesignSystem
|
|||
import Env
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct SidebarEntriesSettingsView: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import SwiftUI
|
||||
import SwiftData
|
||||
import Models
|
||||
import Env
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
struct TagsGroupSettingView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
|
|
|
@ -114,7 +114,6 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
Label("explore.section.trending.links", systemImage: iconName)
|
||||
case .other:
|
||||
EmptyView()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,6 +157,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
class SidebarTabs {
|
||||
struct SidedebarTab: Hashable, Codable {
|
||||
|
@ -203,6 +203,7 @@ class SidebarTabs {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
class iOSTabs {
|
||||
enum TabEntries: String {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct ToolbarTab: ToolbarContent {
|
||||
|
@ -17,7 +17,8 @@ struct ToolbarTab: ToolbarContent {
|
|||
statusEditorToolbarItem(routerPath: routerPath,
|
||||
visibility: userPreferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom != .pad ||
|
||||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact) {
|
||||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
|
||||
{
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,11 +2,11 @@ import Account
|
|||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Models
|
||||
|
||||
class ShareViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
|
|
|
@ -31,7 +31,7 @@ let package = Package(
|
|||
.product(name: "Models", package: "Models"),
|
||||
.product(name: "StatusKit", package: "StatusKit"),
|
||||
.product(name: "Env", package: "Env"),
|
||||
.product(name: "ButtonKit", package: "ButtonKit")
|
||||
.product(name: "ButtonKit", package: "ButtonKit"),
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency"),
|
||||
|
|
|
@ -207,6 +207,7 @@ struct AccountDetailHeaderView: View {
|
|||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
.accessibilityRespondsToUserInteraction(false)
|
||||
movedToView
|
||||
joinedAtView
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
|
@ -311,6 +312,17 @@ struct AccountDetailHeaderView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var movedToView: some View {
|
||||
if let movedTo = viewModel.account?.moved {
|
||||
Button("account.movedto.redirect-\("@\(movedTo.acct)")") {
|
||||
routerPath.navigate(to: .accountDetailWithAccount(account: movedTo))
|
||||
}
|
||||
.font(.scaledCallout)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func makeNoteView(_ note: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
|
|
@ -23,9 +23,6 @@ public struct AccountDetailView: View {
|
|||
@State private var viewModel: AccountDetailViewModel
|
||||
@State private var isCurrentUser: Bool = false
|
||||
@State private var showBlockConfirmation: Bool = false
|
||||
|
||||
@State private var isEditingAccount: Bool = false
|
||||
@State private var isEditingFilters: Bool = false
|
||||
@State private var isEditingRelationshipNote: Bool = false
|
||||
|
||||
@State private var displayTitle: Bool = false
|
||||
|
@ -136,20 +133,14 @@ public struct AccountDetailView: View {
|
|||
viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount)
|
||||
}
|
||||
}
|
||||
.onChange(of: isEditingAccount) { _, newValue in
|
||||
if !newValue {
|
||||
.onChange(of: routerPath.presentedSheet) { oldValue, newValue in
|
||||
if oldValue == .accountEditInfo || newValue == .accountEditInfo {
|
||||
Task {
|
||||
await viewModel.fetchAccount()
|
||||
await preferences.refreshServerPreferences()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isEditingAccount, content: {
|
||||
EditAccountView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingFilters, content: {
|
||||
FiltersListView()
|
||||
})
|
||||
.sheet(isPresented: $isEditingRelationshipNote, content: {
|
||||
EditRelationshipNoteView(accountDetailViewModel: viewModel)
|
||||
})
|
||||
|
@ -220,7 +211,6 @@ public struct AccountDetailView: View {
|
|||
AvatarView(account.avatar, config: .badge)
|
||||
.padding(.leading, -4)
|
||||
.accessibilityLabel(account.safeDisplayName)
|
||||
|
||||
}
|
||||
.accessibilityAddTraits(.isImage)
|
||||
.buttonStyle(.plain)
|
||||
|
@ -288,7 +278,6 @@ public struct AccountDetailView: View {
|
|||
routerPath.presentedSheet = .mentionStatusEditor(account: account,
|
||||
visibility: preferences.postVisibility)
|
||||
#endif
|
||||
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrowshape.turn.up.left")
|
||||
|
@ -308,7 +297,7 @@ public struct AccountDetailView: View {
|
|||
|
||||
if isCurrentUser {
|
||||
Button {
|
||||
isEditingAccount = true
|
||||
routerPath.presentedSheet = .accountEditInfo
|
||||
} label: {
|
||||
Label("account.action.edit-info", systemImage: "pencil")
|
||||
}
|
||||
|
@ -323,7 +312,7 @@ public struct AccountDetailView: View {
|
|||
|
||||
if currentInstance.isFiltersSupported {
|
||||
Button {
|
||||
isEditingFilters = true
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
} label: {
|
||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
|
@ -382,6 +371,7 @@ public struct AccountDetailView: View {
|
|||
}
|
||||
|
||||
extension View {
|
||||
@MainActor
|
||||
func applyAccountDetailsRowStyle(theme: Theme) -> some View {
|
||||
listRowInsets(.init())
|
||||
.listRowSeparator(.hidden)
|
||||
|
|
|
@ -151,7 +151,7 @@ import SwiftUI
|
|||
self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
|
||||
}
|
||||
|
||||
func fetchNewestStatuses(pullToRefresh: Bool) async {
|
||||
func fetchNewestStatuses(pullToRefresh _: Bool) async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
statusesState = .loading
|
||||
|
@ -207,7 +207,7 @@ import SwiftUI
|
|||
statuses.append(contentsOf: newStatuses)
|
||||
if selectedTab == .boosts {
|
||||
let newBoosts = statuses.filter { $0.reblog != nil }
|
||||
self.boosts.append(contentsOf: newBoosts)
|
||||
boosts.append(contentsOf: newBoosts)
|
||||
}
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||
if selectedTab == .boosts {
|
||||
|
@ -253,7 +253,8 @@ import SwiftUI
|
|||
if let event = event as? StreamEventUpdate {
|
||||
if event.status.account.id == currentAccount.account?.id {
|
||||
if (event.status.inReplyToId == nil && selectedTab == .statuses) ||
|
||||
(event.status.inReplyToId != nil && selectedTab == .replies) {
|
||||
(event.status.inReplyToId != nil && selectedTab == .replies)
|
||||
{
|
||||
statuses.insert(event.status, at: 0)
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
|
|
|
@ -153,3 +153,13 @@ public struct AccountsListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
AccountsListRow(viewModel: .init(account: .placeholder(),
|
||||
relationShip: .placeholder()))
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.withPreviewsEnv()
|
||||
.environment(Theme.shared)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Models
|
||||
import Network
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
public enum AccountsListMode {
|
||||
case following(accountId: String), followers(accountId: String)
|
||||
|
@ -144,8 +144,6 @@ public enum AccountsListMode {
|
|||
relationships: relationships,
|
||||
nextPageState: .none)
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import DesignSystem
|
|||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct EditAccountView: View {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Models
|
||||
import Network
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable class EditAccountViewModel {
|
||||
|
@ -33,12 +33,13 @@ import StatusKit
|
|||
|
||||
var isPhotoPickerPresented: Bool = false {
|
||||
didSet {
|
||||
if !isPhotoPickerPresented && mediaPickers.isEmpty {
|
||||
if !isPhotoPickerPresented, mediaPickers.isEmpty {
|
||||
isChangingAvatar = false
|
||||
isChangingHeader = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isChangingAvatar: Bool = false
|
||||
var isChangingHeader: Bool = false
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import Foundation
|
|||
import Models
|
||||
import Network
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable public class FollowButtonViewModel {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
public struct ListsListView: View {
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
|
@ -43,4 +43,3 @@ public struct ListsListView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import StatusKit
|
||||
import Network
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import DesignSystem
|
||||
import Network
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct AccountStatusesListView: View {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import StatusKit
|
||||
import Network
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
|
@ -40,7 +40,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
|||
self.mode = mode
|
||||
}
|
||||
|
||||
public func fetchNewestStatuses(pullToRefresh: Bool) async {
|
||||
public func fetchNewestStatuses(pullToRefresh _: Bool) async {
|
||||
guard let client else { return }
|
||||
statusesState = .loading
|
||||
do {
|
||||
|
@ -63,11 +63,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
|
|||
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
|
||||
}
|
||||
|
||||
public func statusDidAppear(status: Status) {
|
||||
public func statusDidAppear(status _: Status) {}
|
||||
|
||||
}
|
||||
|
||||
public func statusDidDisappear(status: Status) {
|
||||
|
||||
}
|
||||
public func statusDidDisappear(status _: Status) {}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
public struct FollowedTagsListView: View {
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
|
@ -32,4 +32,3 @@ public struct FollowedTagsListView: View {
|
|||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,11 +33,11 @@ public struct AppAccountsSelectorView: View {
|
|||
|
||||
public init(routerPath: RouterPath,
|
||||
accountCreationEnabled: Bool = true,
|
||||
avatarConfig: AvatarView.FrameConfig = .badge)
|
||||
avatarConfig: AvatarView.FrameConfig? = nil)
|
||||
{
|
||||
self.routerPath = routerPath
|
||||
self.accountCreationEnabled = accountCreationEnabled
|
||||
self.avatarConfig = avatarConfig
|
||||
self.avatarConfig = avatarConfig ?? .badge
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
|
|
@ -48,7 +48,7 @@ public struct ConversationsListView: View {
|
|||
Divider()
|
||||
}
|
||||
} else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError {
|
||||
EmptyView(iconName: "tray",
|
||||
PlaceholderView(iconName: "tray",
|
||||
title: "conversations.empty.title",
|
||||
message: "conversations.empty.message")
|
||||
} else if viewModel.isError {
|
||||
|
|
|
@ -19,7 +19,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
.package(name: "Models", path: "../Models"),
|
||||
.package(name: "Env", path: "../Env"),
|
||||
.package(url: "https://github.com/kean/Nuke", from: "12.0.0"),
|
||||
.package(url: "https://github.com/kean/Nuke", from: "12.4.0"),
|
||||
.package(url: "https://github.com/divadretlaw/EmojiText", from: "4.0.0"),
|
||||
],
|
||||
targets: [
|
||||
|
|
|
@ -201,4 +201,3 @@ public struct ThreadsLight: ColorSet {
|
|||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import Combine
|
|||
import UIKit
|
||||
|
||||
@Observable
|
||||
public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
||||
@MainActor public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
||||
public var window: UIWindow?
|
||||
#if os(visionOS)
|
||||
public private(set) var windowWidth: CGFloat = 0
|
||||
|
@ -47,7 +47,7 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
|||
}
|
||||
|
||||
private static var observedSceneDelegate: Set<SceneDelegate> = []
|
||||
private static let observer = Task {
|
||||
private static let observer = Task { @MainActor in
|
||||
while true {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
for delegate in observedSceneDelegate {
|
||||
|
@ -70,7 +70,6 @@ public class SceneDelegate: NSObject, UIWindowSceneDelegate, Sendable {
|
|||
delegate.windowHeight = newHeight
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
@Observable public class Theme {
|
||||
class ThemeStorage {
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class Theme {
|
||||
final class ThemeStorage {
|
||||
enum ThemeKey: String {
|
||||
case colorScheme, tint, label, primaryBackground, secondaryBackground
|
||||
case avatarPosition2, avatarShape2, statusActionsDisplay, statusDisplayStyle
|
||||
|
@ -167,12 +169,14 @@ import SwiftUI
|
|||
public var tintColor: Color {
|
||||
didSet {
|
||||
themeStorage.tintColor = tintColor
|
||||
computeContrastingTintColor()
|
||||
}
|
||||
}
|
||||
|
||||
public var primaryBackgroundColor: Color {
|
||||
didSet {
|
||||
themeStorage.primaryBackgroundColor = primaryBackgroundColor
|
||||
computeContrastingTintColor()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,6 +189,31 @@ import SwiftUI
|
|||
public var labelColor: Color {
|
||||
didSet {
|
||||
themeStorage.labelColor = labelColor
|
||||
computeContrastingTintColor()
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var contrastingTintColor: Color
|
||||
|
||||
// set contrastingTintColor to either labelColor or primaryBackgroundColor, whichever contrasts
|
||||
// better against the tintColor
|
||||
private func computeContrastingTintColor() {
|
||||
func luminance(_ color: Color.Resolved) -> Float {
|
||||
return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue
|
||||
}
|
||||
|
||||
let resolvedTintColor = tintColor.resolve(in: .init())
|
||||
let resolvedLabelColor = labelColor.resolve(in: .init())
|
||||
let resolvedPrimaryBackgroundColor = primaryBackgroundColor.resolve(in: .init())
|
||||
|
||||
let tintLuminance = luminance(resolvedTintColor)
|
||||
let labelLuminance = luminance(resolvedLabelColor)
|
||||
let primaryBackgroundLuminance = luminance(resolvedPrimaryBackgroundColor)
|
||||
|
||||
if abs(tintLuminance - labelLuminance) > abs(tintLuminance - primaryBackgroundLuminance) {
|
||||
contrastingTintColor = labelColor
|
||||
} else {
|
||||
contrastingTintColor = primaryBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,6 +310,7 @@ import SwiftUI
|
|||
primaryBackgroundColor = themeStorage.primaryBackgroundColor
|
||||
secondaryBackgroundColor = themeStorage.secondaryBackgroundColor
|
||||
labelColor = themeStorage.labelColor
|
||||
contrastingTintColor = .red // real work done in computeContrastingTintColor()
|
||||
avatarPosition = themeStorage.avatarPosition
|
||||
avatarShape = themeStorage.avatarShape
|
||||
storedSet = themeStorage.storedSet
|
||||
|
@ -293,6 +323,8 @@ import SwiftUI
|
|||
chosenFontData = themeStorage.chosenFontData
|
||||
statusActionSecondary = themeStorage.statusActionSecondary
|
||||
selectedSet = storedSet
|
||||
|
||||
computeContrastingTintColor()
|
||||
}
|
||||
|
||||
public static var allColorSet: [ColorSet] {
|
||||
|
@ -310,7 +342,7 @@ import SwiftUI
|
|||
ConstellationLight(),
|
||||
ConstellationDark(),
|
||||
ThreadsLight(),
|
||||
ThreadsDark()
|
||||
ThreadsDark(),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import SwiftUI
|
|||
#endif
|
||||
|
||||
public extension View {
|
||||
func applyTheme(_ theme: Theme) -> some View {
|
||||
@MainActor func applyTheme(_ theme: Theme) -> some View {
|
||||
modifier(ThemeApplier(theme: theme))
|
||||
}
|
||||
}
|
||||
|
@ -77,16 +77,14 @@ struct ThemeApplier: ViewModifier {
|
|||
}
|
||||
|
||||
private func setWindowUserInterfaceStyle(_ userInterfaceStyle: UIUserInterfaceStyle) {
|
||||
allWindows()
|
||||
.forEach {
|
||||
$0.overrideUserInterfaceStyle = userInterfaceStyle
|
||||
for window in allWindows() {
|
||||
window.overrideUserInterfaceStyle = userInterfaceStyle
|
||||
}
|
||||
}
|
||||
|
||||
private func setWindowTint(_ color: Color) {
|
||||
allWindows()
|
||||
.forEach {
|
||||
$0.tintColor = UIColor(color)
|
||||
for window in allWindows() {
|
||||
window.tintColor = UIColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import Nuke
|
|||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct AccountPopoverView: View {
|
||||
let account: Account
|
||||
let theme: Theme // using `@Environment(Theme.self) will crash the SwiftUI preview
|
||||
|
|
|
@ -33,7 +33,8 @@ public struct AvatarView: View {
|
|||
self.config = config
|
||||
}
|
||||
|
||||
public struct FrameConfig: Equatable {
|
||||
@MainActor
|
||||
public struct FrameConfig: Equatable, Sendable {
|
||||
public let size: CGSize
|
||||
public var width: CGFloat { size.width }
|
||||
public var height: CGFloat { size.height }
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
public struct EmptyView: View {
|
||||
public let iconName: String
|
||||
public let title: LocalizedStringKey
|
||||
public let message: LocalizedStringKey
|
||||
|
||||
public init(iconName: String, title: LocalizedStringKey, message: LocalizedStringKey) {
|
||||
self.iconName = iconName
|
||||
self.title = title
|
||||
self.message = message
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack {
|
||||
Image(systemName: iconName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: 50)
|
||||
Text(title)
|
||||
.font(.scaledTitle)
|
||||
.padding(.top, 16)
|
||||
Text(message)
|
||||
.font(.scaledSubheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.top, 100)
|
||||
.padding(.layoutPadding)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
|
@ -42,3 +42,9 @@ public struct ErrorView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ErrorView(title: "Error",
|
||||
message: "Error loading. Please try again",
|
||||
buttonTitle: "Retry") {}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct NextPageView: View {
|
||||
@State private var isLoadingNextPage: Bool = false
|
||||
@State private var showRetry: Bool = false
|
||||
|
||||
let loadNextPage: (() async throws -> Void)
|
||||
let loadNextPage: () async throws -> Void
|
||||
|
||||
public init(loadNextPage: @escaping (() async throws -> Void)) {
|
||||
self.loadNextPage = loadNextPage
|
||||
|
@ -50,3 +51,11 @@ public struct NextPageView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
Text("Item 1")
|
||||
NextPageView {}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import SwiftUI
|
||||
|
||||
public struct PlaceholderView: View {
|
||||
public let iconName: String
|
||||
public let title: LocalizedStringKey
|
||||
public let message: LocalizedStringKey
|
||||
|
||||
public init(iconName: String, title: LocalizedStringKey, message: LocalizedStringKey) {
|
||||
self.iconName = iconName
|
||||
self.title = title
|
||||
self.message = message
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ContentUnavailableView(title,
|
||||
systemImage: iconName,
|
||||
description: Text(message))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PlaceholderView(iconName: "square.and.arrow.up.trianglebadge.exclamationmark",
|
||||
title: "Nothing to see",
|
||||
message: "This is a preview. Please try again.")
|
||||
}
|
|
@ -11,7 +11,7 @@ public struct ScrollToView: View {
|
|||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
HStack { SwiftUI.EmptyView() }
|
||||
HStack { EmptyView() }
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.init())
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
/*! @copyright 2021 Medium */
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// Source: https://www.fivestars.blog/articles/scrollview-offset/
|
||||
|
||||
public struct ScrollViewOffsetReader<Content: View>: View {
|
||||
let onOffsetChange: (CGFloat) -> Void
|
||||
let content: () -> Content
|
||||
|
||||
public init(
|
||||
onOffsetChange: @escaping (CGFloat) -> Void,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.onOffsetChange = onOffsetChange
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ScrollView {
|
||||
offsetReader
|
||||
content()
|
||||
.padding(.top, -8)
|
||||
}
|
||||
.coordinateSpace(name: "frameLayer")
|
||||
.onPreferenceChange(OffsetPreferenceKey.self, perform: onOffsetChange)
|
||||
}
|
||||
|
||||
var offsetReader: some View {
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.preference(
|
||||
key: OffsetPreferenceKey.self,
|
||||
value: proxy.frame(in: .named("frameLayer")).minY
|
||||
)
|
||||
}
|
||||
.frame(height: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private struct OffsetPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGFloat = .zero
|
||||
static func reduce(value _: inout CGFloat, nextValue _: () -> CGFloat) {}
|
||||
}
|
15
Packages/Env/Sources/Env/PreviewEnv.swift
Normal file
15
Packages/Env/Sources/Env/PreviewEnv.swift
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Network
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public extension View {
|
||||
func withPreviewsEnv() -> some View {
|
||||
environment(RouterPath())
|
||||
.environment(Client(server: ""))
|
||||
.environment(CurrentAccount.shared)
|
||||
.environment(UserPreferences.shared)
|
||||
.environment(CurrentInstance.shared)
|
||||
.environment(PushNotificationsService.shared)
|
||||
.environment(QuickLook.shared)
|
||||
}
|
||||
}
|
|
@ -38,7 +38,15 @@ public enum WindowDestinationMedia: Hashable, Codable {
|
|||
case mediaViewer(attachments: [MediaAttachment], selectedAttachment: MediaAttachment)
|
||||
}
|
||||
|
||||
public enum SheetDestination: Identifiable {
|
||||
public enum SheetDestination: Identifiable, Hashable {
|
||||
public static func == (lhs: SheetDestination, rhs: SheetDestination) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
case newStatusEditor(visibility: Models.Visibility)
|
||||
case editStatusEditor(status: Status)
|
||||
case replyToStatusEditor(status: Status)
|
||||
|
@ -60,6 +68,8 @@ public enum SheetDestination: Identifiable {
|
|||
case shareImage(image: UIImage, status: Status)
|
||||
case editTagGroup(tagGroup: TagGroup, onSaved: ((TagGroup) -> Void)?)
|
||||
case timelineContentFilter
|
||||
case accountEditInfo
|
||||
case accountFiltersList
|
||||
|
||||
public var id: String {
|
||||
switch self {
|
||||
|
@ -90,6 +100,10 @@ public enum SheetDestination: Identifiable {
|
|||
"settings"
|
||||
case .timelineContentFilter:
|
||||
"timelineContentFilter"
|
||||
case .accountEditInfo:
|
||||
"accountEditInfo"
|
||||
case .accountFiltersList:
|
||||
"accountFiltersList"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,7 +162,8 @@ public enum SheetDestination: Identifiable {
|
|||
return .handled
|
||||
} else if url.lastPathComponent.first == "@",
|
||||
let host = url.host,
|
||||
!host.hasPrefix("www") {
|
||||
!host.hasPrefix("www")
|
||||
{
|
||||
let acct = "\(url.lastPathComponent)@\(host)"
|
||||
Task {
|
||||
await navigateToAccountFrom(acct: acct, url: url)
|
||||
|
|
|
@ -65,7 +65,7 @@ import OSLog
|
|||
connect()
|
||||
}
|
||||
watchedStreams = streams
|
||||
streams.forEach { stream in
|
||||
for stream in streams {
|
||||
sendMessage(message: StreamMessage(type: "subscribe", stream: stream.rawValue))
|
||||
}
|
||||
}
|
||||
|
@ -159,19 +159,19 @@ import OSLog
|
|||
|
||||
public func emmitDeleteEvent(for status: String) {
|
||||
let event = StreamEventDelete(status: status)
|
||||
self.events.append(event)
|
||||
self.latestEvent = event
|
||||
events.append(event)
|
||||
latestEvent = event
|
||||
}
|
||||
|
||||
public func emmitEditEvent(for status: Status) {
|
||||
let event = StreamEventStatusUpdate(status: status)
|
||||
self.events.append(event)
|
||||
self.latestEvent = event
|
||||
events.append(event)
|
||||
latestEvent = event
|
||||
}
|
||||
|
||||
public func emmitPostEvent(for status: Status) {
|
||||
let event = StreamEventUpdate(status: status)
|
||||
self.events.append(event)
|
||||
self.latestEvent = event
|
||||
events.append(event)
|
||||
latestEvent = event
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,7 +183,6 @@ import SwiftUI
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public var alwaysUseDeepl: Bool {
|
||||
didSet {
|
||||
storage.alwaysUseDeepl = alwaysUseDeepl
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@testable import Env
|
||||
import XCTest
|
||||
import SwiftUI
|
||||
import Network
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
final class RouterTests: XCTestCase {
|
||||
|
|
|
@ -32,7 +32,7 @@ public struct ExploreView: View {
|
|||
} else if !viewModel.searchQuery.isEmpty {
|
||||
if let results = viewModel.results[viewModel.searchQuery] {
|
||||
if results.isEmpty, !viewModel.isSearching {
|
||||
EmptyView(iconName: "magnifyingglass",
|
||||
PlaceholderView(iconName: "magnifyingglass",
|
||||
title: "explore.search.empty.title",
|
||||
message: "explore.search.empty.message")
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
|
@ -53,7 +53,7 @@ public struct ExploreView: View {
|
|||
.id(UUID())
|
||||
}
|
||||
} else if viewModel.allSectionsEmpty {
|
||||
EmptyView(iconName: "magnifyingglass",
|
||||
PlaceholderView(iconName: "magnifyingglass",
|
||||
title: "explore.search.title",
|
||||
message: "explore.search.message-\(client.server)")
|
||||
#if !os(visionOS)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import Network
|
||||
|
||||
public struct TrendingLinksListView: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
enum DisplayType {
|
||||
case image
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import AVKit
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import Models
|
||||
|
||||
@MainActor
|
||||
@Observable public class MediaUIAttachmentVideoViewModel {
|
||||
|
@ -23,7 +23,7 @@ import Models
|
|||
#if !os(visionOS)
|
||||
player?.preventsDisplaySleepDuringVideoPlayback = false
|
||||
#endif
|
||||
if (autoPlay || forceAutoPlay) && !isCompact {
|
||||
if autoPlay || forceAutoPlay, !isCompact {
|
||||
player?.play()
|
||||
isPlaying = true
|
||||
} else {
|
||||
|
@ -179,7 +179,8 @@ public struct MediaUIAttachmentVideoView: View {
|
|||
!viewModel.forceAutoPlay,
|
||||
!isFullScreen,
|
||||
!viewModel.isPlaying,
|
||||
!isCompact {
|
||||
!isCompact
|
||||
{
|
||||
Button(action: {
|
||||
viewModel.play()
|
||||
}, label: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import AVFoundation
|
||||
import Models
|
||||
import Nuke
|
||||
import QuickLook
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
||||
public struct MediaUIView: View, @unchecked Sendable {
|
||||
private let data: [DisplayData]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import SwiftUI
|
||||
import NukeUI
|
||||
import Nuke
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
struct QuickLookToolbarItem: ToolbarContent, @unchecked Sendable {
|
||||
let itemUrl: URL
|
||||
|
|
|
@ -61,6 +61,7 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
|||
public let source: Source?
|
||||
public let bot: Bool
|
||||
public let discoverable: Bool?
|
||||
public let moved: Account?
|
||||
|
||||
public var haveAvatar: Bool {
|
||||
avatar.lastPathComponent != "missing.png"
|
||||
|
@ -70,7 +71,7 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
|||
header.lastPathComponent != "missing.png"
|
||||
}
|
||||
|
||||
public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil) {
|
||||
public init(id: String, username: String, displayName: String?, avatar: URL, header: URL, acct: String, note: HTMLString, createdAt: ServerDate, followersCount: Int, followingCount: Int, statusesCount: Int, lastStatusAt: String? = nil, fields: [Account.Field], locked: Bool, emojis: [Emoji], url: URL? = nil, source: Account.Source? = nil, bot: Bool, discoverable: Bool? = nil, moved: Account? = nil) {
|
||||
self.id = id
|
||||
self.username = username
|
||||
self.displayName = displayName
|
||||
|
@ -90,11 +91,12 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
|||
self.source = source
|
||||
self.bot = bot
|
||||
self.discoverable = discoverable
|
||||
self.moved = moved
|
||||
|
||||
if let displayName, !displayName.isEmpty {
|
||||
self.cachedDisplayName = .init(stringValue: displayName)
|
||||
cachedDisplayName = .init(stringValue: displayName)
|
||||
} else {
|
||||
self.cachedDisplayName = .init(stringValue: "@\(username)")
|
||||
cachedDisplayName = .init(stringValue: "@\(username)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,33 +120,36 @@ public final class Account: Codable, Identifiable, Hashable, Sendable, Equatable
|
|||
case source
|
||||
case bot
|
||||
case discoverable
|
||||
case moved
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try container.decode(String.self, forKey: .id)
|
||||
self.username = try container.decode(String.self, forKey: .username)
|
||||
self.displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
|
||||
self.avatar = try container.decode(URL.self, forKey: .avatar)
|
||||
self.header = try container.decode(URL.self, forKey: .header)
|
||||
self.acct = try container.decode(String.self, forKey: .acct)
|
||||
self.note = try container.decode(HTMLString.self, forKey: .note)
|
||||
self.createdAt = try container.decode(ServerDate.self, forKey: .createdAt)
|
||||
self.followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount)
|
||||
self.followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount)
|
||||
self.statusesCount = try container.decodeIfPresent(Int.self, forKey: .statusesCount)
|
||||
self.lastStatusAt = try container.decodeIfPresent(String.self, forKey: .lastStatusAt)
|
||||
self.fields = try container.decode([Account.Field].self, forKey: .fields)
|
||||
self.locked = try container.decode(Bool.self, forKey: .locked)
|
||||
self.emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||
self.url = try container.decodeIfPresent(URL.self, forKey: .url)
|
||||
self.source = try container.decodeIfPresent(Account.Source.self, forKey: .source)
|
||||
self.bot = try container.decode(Bool.self, forKey: .bot)
|
||||
self.discoverable = try container.decodeIfPresent(Bool.self, forKey: .discoverable)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
username = try container.decode(String.self, forKey: .username)
|
||||
displayName = try container.decodeIfPresent(String.self, forKey: .displayName)
|
||||
avatar = try container.decode(URL.self, forKey: .avatar)
|
||||
header = try container.decode(URL.self, forKey: .header)
|
||||
acct = try container.decode(String.self, forKey: .acct)
|
||||
note = try container.decode(HTMLString.self, forKey: .note)
|
||||
createdAt = try container.decode(ServerDate.self, forKey: .createdAt)
|
||||
followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount)
|
||||
followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount)
|
||||
statusesCount = try container.decodeIfPresent(Int.self, forKey: .statusesCount)
|
||||
lastStatusAt = try container.decodeIfPresent(String.self, forKey: .lastStatusAt)
|
||||
fields = try container.decode([Account.Field].self, forKey: .fields)
|
||||
locked = try container.decode(Bool.self, forKey: .locked)
|
||||
emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||
url = try container.decodeIfPresent(URL.self, forKey: .url)
|
||||
source = try container.decodeIfPresent(Account.Source.self, forKey: .source)
|
||||
bot = try container.decode(Bool.self, forKey: .bot)
|
||||
discoverable = try container.decodeIfPresent(Bool.self, forKey: .discoverable)
|
||||
moved = try container.decodeIfPresent(Account.self, forKey: .moved)
|
||||
|
||||
if let displayName, !displayName.isEmpty {
|
||||
self.cachedDisplayName = .init(stringValue: displayName)
|
||||
cachedDisplayName = .init(stringValue: displayName)
|
||||
} else {
|
||||
self.cachedDisplayName = .init(stringValue: "@\(username)")
|
||||
cachedDisplayName = .init(stringValue: "@\(username)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import Foundation
|
|||
import Models
|
||||
import Observation
|
||||
import os
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
@Observable public final class Client: Equatable, Identifiable, Hashable, @unchecked Sendable {
|
||||
public static func == (lhs: Client, rhs: Client) -> Bool {
|
||||
|
@ -286,7 +286,8 @@ import OSLog
|
|||
method: String,
|
||||
mimeType: String,
|
||||
filename: String,
|
||||
data: Data) throws -> URLRequest {
|
||||
data: Data) throws -> URLRequest
|
||||
{
|
||||
let url = try makeURL(endpoint: endpoint, forceVersion: version)
|
||||
var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
|
||||
let boundary = UUID().uuidString
|
||||
|
|
|
@ -34,8 +34,8 @@ public struct InstanceSocialClient: Sendable {
|
|||
}
|
||||
}
|
||||
|
||||
extension Array where Self.Element == InstanceSocial {
|
||||
fileprivate func sorted(by keyword: String) -> Self {
|
||||
private extension Array where Self.Element == InstanceSocial {
|
||||
func sorted(by keyword: String) -> Self {
|
||||
let keyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var newArray = self
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ extension Models.Notification.NotificationType {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func tintColor(isPrivate: Bool) -> Color {
|
||||
if isPrivate {
|
||||
return Color.orange.opacity(0.80)
|
||||
|
|
|
@ -153,7 +153,7 @@ public struct NotificationsListView: View {
|
|||
|
||||
case let .display(notifications, nextPageState):
|
||||
if notifications.isEmpty {
|
||||
EmptyView(iconName: "bell.slash",
|
||||
PlaceholderView(iconName: "bell.slash",
|
||||
title: "notifications.empty.title",
|
||||
message: "notifications.empty.message")
|
||||
#if !os(visionOS)
|
||||
|
|
|
@ -37,5 +37,4 @@ extension StatusEditor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -159,7 +159,6 @@ extension StatusEditor {
|
|||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||
.disabled(viewModel.showPoll)
|
||||
|
||||
|
||||
Button {
|
||||
// all SEVM have the same visibility value
|
||||
followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility)))
|
||||
|
@ -188,7 +187,6 @@ extension StatusEditor {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if preferences.isOpenAIEnabled {
|
||||
AIMenu.disabled(!viewModel.canPost)
|
||||
}
|
||||
|
@ -268,7 +266,5 @@ extension StatusEditor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import DesignSystem
|
||||
import EmojiText
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Models
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor {
|
||||
|
||||
@MainActor
|
||||
struct AutoCompleteView: View {
|
||||
@Environment(\.modelContext) var context
|
||||
|
@ -22,7 +21,8 @@ extension StatusEditor {
|
|||
var body: some View {
|
||||
if !viewModel.mentionsSuggestions.isEmpty ||
|
||||
!viewModel.tagsSuggestions.isEmpty ||
|
||||
(viewModel.showRecentsTagsInline && !recentTags.isEmpty) {
|
||||
(viewModel.showRecentsTagsInline && !recentTags.isEmpty)
|
||||
{
|
||||
VStack {
|
||||
HStack {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import DesignSystem
|
||||
import EmojiText
|
||||
import Env
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Models
|
||||
import SwiftData
|
||||
import Env
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor.AutoCompleteView {
|
||||
@MainActor
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import DesignSystem
|
||||
import EmojiText
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Models
|
||||
import SwiftData
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor.AutoCompleteView {
|
||||
struct MentionsView: View {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import DesignSystem
|
||||
import EmojiText
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Models
|
||||
import SwiftData
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor.AutoCompleteView {
|
||||
struct RecentTagsView: View {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import DesignSystem
|
||||
import EmojiText
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Models
|
||||
import SwiftData
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor.AutoCompleteView {
|
||||
struct RemoteTagsView: View {
|
||||
|
|
|
@ -35,5 +35,4 @@ extension StatusEditor {
|
|||
Coordinator(picker: self)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,5 +7,4 @@ extension StatusEditor {
|
|||
let categoryName: String
|
||||
var emojis: [Emoji]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import AVFoundation
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension StatusEditor {
|
||||
public actor Compressor {
|
||||
public extension StatusEditor {
|
||||
actor Compressor {
|
||||
public init() {}
|
||||
|
||||
enum CompressorError: Error {
|
||||
|
@ -106,5 +106,4 @@ extension StatusEditor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUI
|
||||
import Models
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor {
|
||||
|
||||
@MainActor
|
||||
struct CustomEmojisView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUI
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor {
|
||||
|
||||
@MainActor
|
||||
struct LangButton: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
|
|
|
@ -13,5 +13,4 @@ extension StatusEditor {
|
|||
let mediaAttachment: MediaAttachment?
|
||||
let error: Error?
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -171,5 +171,4 @@ extension StatusEditor {
|
|||
return translation?.content.asRawText
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -249,5 +249,4 @@ extension StatusEditor {
|
|||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -121,5 +121,4 @@ extension StatusEditor {
|
|||
return index == count - 1 && count < maxEntries
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -86,10 +86,10 @@ extension StatusEditor {
|
|||
|
||||
static var transferRepresentation: some TransferRepresentation {
|
||||
FileRepresentation(importedContentType: .movie) { receivedTransferrable in
|
||||
return MovieFileTranseferable(url: receivedTransferrable.localURL)
|
||||
MovieFileTranseferable(url: receivedTransferrable.localURL)
|
||||
}
|
||||
FileRepresentation(importedContentType: .video) { receivedTransferrable in
|
||||
return MovieFileTranseferable(url: receivedTransferrable.localURL)
|
||||
MovieFileTranseferable(url: receivedTransferrable.localURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ extension StatusEditor {
|
|||
|
||||
static var transferRepresentation: some TransferRepresentation {
|
||||
FileRepresentation(importedContentType: .gif) { receivedTransferrable in
|
||||
return GifFileTranseferable(url: receivedTransferrable.localURL)
|
||||
GifFileTranseferable(url: receivedTransferrable.localURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ public extension StatusEditor {
|
|||
|
||||
public static var transferRepresentation: some TransferRepresentation {
|
||||
FileRepresentation(importedContentType: .image) { receivedTransferrable in
|
||||
return ImageFileTranseferable(url: receivedTransferrable.localURL)
|
||||
ImageFileTranseferable(url: receivedTransferrable.localURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,15 +141,15 @@ public extension StatusEditor {
|
|||
|
||||
public extension ReceivedTransferredFile {
|
||||
var localURL: URL {
|
||||
if self.isOriginalFile {
|
||||
if isOriginalFile {
|
||||
return file
|
||||
}
|
||||
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(self.file.pathExtension)")
|
||||
try? FileManager.default.copyItem(at: self.file, to: copy)
|
||||
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(file.pathExtension)")
|
||||
try? FileManager.default.copyItem(at: file, to: copy)
|
||||
return copy
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension URL {
|
||||
func mimeType() -> String {
|
||||
if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {
|
||||
|
|
|
@ -49,5 +49,4 @@ extension StatusEditor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,5 +4,4 @@ extension StatusEditor {
|
|||
enum EditorFocusState: Hashable {
|
||||
case main, followUp(index: UUID)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -159,7 +159,6 @@ extension StatusEditor {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var characterCountAndLangView: some View {
|
||||
let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength
|
||||
|
@ -223,5 +222,4 @@ extension StatusEditor {
|
|||
Task { await viewModel.fetchCustomEmojis() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ import StoreKit
|
|||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
extension StatusEditor {
|
||||
public extension StatusEditor {
|
||||
@MainActor
|
||||
public struct MainView: View {
|
||||
struct MainView: View {
|
||||
@Environment(AppAccountsManager.self) private var appAccounts
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(Theme.self) private var theme
|
||||
|
@ -151,5 +151,4 @@ extension StatusEditor {
|
|||
.presentationBackgroundInteraction(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,5 +30,4 @@ extension StatusEditor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import StoreKit
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
extension StatusEditor {
|
||||
@MainActor
|
||||
|
|
|
@ -7,10 +7,9 @@ import Network
|
|||
import PhotosUI
|
||||
import SwiftUI
|
||||
|
||||
extension StatusEditor {
|
||||
|
||||
public extension StatusEditor {
|
||||
@MainActor
|
||||
@Observable public class ViewModel: NSObject, Identifiable {
|
||||
@Observable class ViewModel: NSObject, Identifiable {
|
||||
public let id = UUID()
|
||||
|
||||
var mode: Mode
|
||||
|
@ -132,15 +131,16 @@ extension StatusEditor {
|
|||
}
|
||||
|
||||
var allMediaHasDescription: Bool {
|
||||
var everyMediaHasAltText: Bool = true;
|
||||
mediaContainers.forEach { mediaContainer in
|
||||
if (((mediaContainer.mediaAttachment?.description) == nil) ||
|
||||
mediaContainer.mediaAttachment?.description?.count == 0) {
|
||||
var everyMediaHasAltText = true
|
||||
for mediaContainer in mediaContainers {
|
||||
if ((mediaContainer.mediaAttachment?.description) == nil) ||
|
||||
mediaContainer.mediaAttachment?.description?.count == 0
|
||||
{
|
||||
everyMediaHasAltText = false
|
||||
}
|
||||
}
|
||||
|
||||
return everyMediaHasAltText;
|
||||
return everyMediaHasAltText
|
||||
}
|
||||
|
||||
var shouldDisplayDismissWarning: Bool {
|
||||
|
@ -200,12 +200,12 @@ extension StatusEditor {
|
|||
func postStatus() async -> Status? {
|
||||
guard let client else { return nil }
|
||||
do {
|
||||
if (!allMediaHasDescription && UserPreferences.shared.appRequireAltText) {
|
||||
if !allMediaHasDescription && UserPreferences.shared.appRequireAltText {
|
||||
throw PostError.missingAltText
|
||||
}
|
||||
|
||||
if postingTimer == nil {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
|
||||
Task { @MainActor in
|
||||
if self.postingProgress < 100 {
|
||||
self.postingProgress += 0.5
|
||||
|
@ -616,7 +616,8 @@ extension StatusEditor {
|
|||
if !tagsSuggestions.isEmpty ||
|
||||
!mentionsSuggestions.isEmpty ||
|
||||
currentSuggestionRange != nil ||
|
||||
showRecentsTagsInline {
|
||||
showRecentsTagsInline
|
||||
{
|
||||
withAnimation {
|
||||
tagsSuggestions = []
|
||||
mentionsSuggestions = []
|
||||
|
|
|
@ -8,7 +8,7 @@ private func stripToPureLanguage(inText: String) -> String {
|
|||
|
||||
var resultStr = inText
|
||||
|
||||
[hashtagRegex, emojiRegex, atRegex].forEach { regex in
|
||||
for regex in [hashtagRegex, emojiRegex, atRegex] {
|
||||
let splitArray = resultStr.split(separator: regex, omittingEmptySubsequences: true)
|
||||
resultStr = splitArray.joined() as String
|
||||
}
|
||||
|
|
|
@ -188,19 +188,20 @@ public struct StatusPollView: View {
|
|||
private struct _PercentWidthLayout: Layout {
|
||||
let percent: CGFloat
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) -> CGSize {
|
||||
guard let view = subviews.first else { return CGSize.zero }
|
||||
return view.sizeThatFits(proposal)
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) {
|
||||
guard let view = subviews.first,
|
||||
let width = proposal.width
|
||||
else { return }
|
||||
|
||||
view.place(
|
||||
at: bounds.origin,
|
||||
proposal: ProposedViewSize(width: percent / 100 * width, height: proposal.height))
|
||||
proposal: ProposedViewSize(width: percent / 100 * width, height: proposal.height)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
/// A utility that creates a suitable combined accessibility label for a `StatusRowView` that is not focused.
|
||||
@MainActor
|
||||
|
|
|
@ -28,7 +28,7 @@ public struct StatusRowView: View {
|
|||
private let context: Context
|
||||
|
||||
public init(viewModel: StatusRowViewModel, context: Context = .timeline) {
|
||||
self._viewModel = .init(initialValue: viewModel)
|
||||
_viewModel = .init(initialValue: viewModel)
|
||||
self.context = context
|
||||
}
|
||||
|
||||
|
@ -75,10 +75,12 @@ public struct StatusRowView: View {
|
|||
if !isCompact,
|
||||
theme.avatarPosition == .leading
|
||||
{
|
||||
Button {
|
||||
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
|
||||
} label: {
|
||||
AvatarView(viewModel.finalStatus.account.avatar)
|
||||
.accessibility(addTraits: .isButton)
|
||||
.contentShape(Circle())
|
||||
.hoverEffect()
|
||||
.onTapGesture {
|
||||
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: .statusComponentSpacing) {
|
||||
|
@ -98,7 +100,8 @@ public struct StatusRowView: View {
|
|||
}
|
||||
if !reasons.contains(.placeholder),
|
||||
viewModel.showActions, isFocused || theme.statusActionsDisplay != .none,
|
||||
!isInCaptureMode {
|
||||
!isInCaptureMode
|
||||
{
|
||||
StatusRowActionsView(isBlockConfirmationPresented: $isBlockConfirmationPresented,
|
||||
viewModel: viewModel)
|
||||
.tint(isFocused ? theme.tintColor : .gray)
|
||||
|
@ -194,7 +197,8 @@ public struct StatusRowView: View {
|
|||
)
|
||||
})
|
||||
.confirmationDialog("",
|
||||
isPresented: $isBlockConfirmationPresented) {
|
||||
isPresented: $isBlockConfirmationPresented)
|
||||
{
|
||||
Button("account.action.block", role: .destructive) {
|
||||
Task {
|
||||
do {
|
||||
|
@ -329,3 +333,25 @@ public struct StatusRowView: View {
|
|||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
StatusRowView(viewModel:
|
||||
.init(status: .placeholder(),
|
||||
client: .init(server: ""),
|
||||
routerPath: RouterPath()),
|
||||
context: .timeline)
|
||||
StatusRowView(viewModel:
|
||||
.init(status: .placeholder(),
|
||||
client: .init(server: ""),
|
||||
routerPath: RouterPath()),
|
||||
context: .timeline)
|
||||
StatusRowView(viewModel:
|
||||
.init(status: .placeholder(),
|
||||
client: .init(server: ""),
|
||||
routerPath: RouterPath()),
|
||||
context: .timeline)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.withPreviewsEnv()
|
||||
.environment(Theme.shared)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Nuke
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
@MainActor
|
||||
public struct StatusRowCardView: View {
|
||||
|
@ -50,13 +50,14 @@ public struct StatusRowCardView: View {
|
|||
} label: {
|
||||
if let title = card.title, let url = URL(string: card.url) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
let sitesWithIcons = ["apps.apple.com", "music.apple.com", "open.spotify.com"]
|
||||
let sitesWithIcons = ["apps.apple.com", "music.apple.com", "podcasts.apple.com", "open.spotify.com"]
|
||||
if isCompact {
|
||||
compactLinkPreview(title, url)
|
||||
} else if (UIDevice.current.userInterfaceIdiom == .pad ||
|
||||
} else if UIDevice.current.userInterfaceIdiom == .pad ||
|
||||
UIDevice.current.userInterfaceIdiom == .mac ||
|
||||
UIDevice.current.userInterfaceIdiom == .vision),
|
||||
let host = url.host(), sitesWithIcons.contains(host) {
|
||||
UIDevice.current.userInterfaceIdiom == .vision,
|
||||
let host = url.host(), sitesWithIcons.contains(host)
|
||||
{
|
||||
iconLinkPreview(title, url)
|
||||
} else {
|
||||
defaultLinkPreview(title, url)
|
||||
|
@ -233,12 +234,10 @@ struct DefaultPreviewImage: View {
|
|||
var body: some View {
|
||||
_Layout(originalWidth: originalWidth, originalHeight: originalHeight) {
|
||||
LazyResizableImage(url: url) { state, _ in
|
||||
Rectangle()
|
||||
.fill(theme.secondaryBackgroundColor)
|
||||
.overlay {
|
||||
if let image = state.image {
|
||||
image.resizable().scaledToFill()
|
||||
}
|
||||
if let image = state.image?.resizable() {
|
||||
Rectangle().fill(theme.secondaryBackgroundColor)
|
||||
.overlay { image.scaledToFill().blur(radius: 50) }
|
||||
.overlay { image.scaledToFit() }
|
||||
}
|
||||
}
|
||||
.accessibilityHidden(true) // This image is decorative
|
||||
|
@ -250,12 +249,12 @@ struct DefaultPreviewImage: View {
|
|||
let originalWidth: CGFloat
|
||||
let originalHeight: CGFloat
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) -> CGSize {
|
||||
guard !subviews.isEmpty else { return CGSize.zero }
|
||||
return calculateSize(proposal)
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) {
|
||||
guard let view = subviews.first else { return }
|
||||
|
||||
let size = calculateSize(proposal)
|
||||
|
@ -263,7 +262,7 @@ struct DefaultPreviewImage: View {
|
|||
}
|
||||
|
||||
private func calculateSize(_ proposal: ProposedViewSize) -> CGSize {
|
||||
switch (proposal.width, proposal.height) {
|
||||
var size = switch (proposal.width, proposal.height) {
|
||||
case (nil, nil):
|
||||
CGSize(width: originalWidth, height: originalWidth)
|
||||
case let (nil, .some(height)):
|
||||
|
@ -277,6 +276,9 @@ struct DefaultPreviewImage: View {
|
|||
CGSize(width: width, height: width / originalWidth * originalHeight)
|
||||
}
|
||||
}
|
||||
|
||||
size.height = min(size.height, 450)
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct StatusRowHeaderView: View {
|
||||
|
@ -15,12 +15,12 @@ struct StatusRowHeaderView: View {
|
|||
let viewModel: StatusRowViewModel
|
||||
var body: some View {
|
||||
HStack(alignment: theme.avatarPosition == .top ? .center : .top) {
|
||||
Button {
|
||||
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
|
||||
} label: {
|
||||
accountView
|
||||
.hoverEffect()
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.onTapGesture {
|
||||
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
Spacer()
|
||||
if !redactionReasons.contains(.placeholder) {
|
||||
dateView
|
||||
|
@ -66,7 +66,8 @@ struct StatusRowHeaderView: View {
|
|||
}
|
||||
if !redactionReasons.contains(.placeholder) {
|
||||
if (theme.displayFullUsername && theme.avatarPosition == .leading) ||
|
||||
theme.avatarPosition == .top {
|
||||
theme.avatarPosition == .top
|
||||
{
|
||||
Text("@\(theme.displayFullUsername ? viewModel.finalStatus.account.acct : viewModel.finalStatus.account.username)")
|
||||
.font(.scaledFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
|
|
@ -212,7 +212,7 @@ struct BlurOverLay: View {
|
|||
.matchedGeometryEffect(id: "eye", in: buttonSpace)
|
||||
}
|
||||
.lineLimit(1)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.foregroundColor(theme.contrastingTintColor)
|
||||
} else {
|
||||
Image(systemName: "eye.slash")
|
||||
.transition(.opacity)
|
||||
|
@ -468,7 +468,7 @@ private struct FeaturedImagePreView: View {
|
|||
self.maxSize = maxSize
|
||||
}
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) -> CGSize {
|
||||
guard !subviews.isEmpty else { return CGSize.zero }
|
||||
|
||||
if let maxSize { return maxSize }
|
||||
|
@ -476,7 +476,7 @@ private struct FeaturedImagePreView: View {
|
|||
return calculateSize(proposal)
|
||||
}
|
||||
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) {
|
||||
guard let view = subviews.first else { return }
|
||||
|
||||
let size = if let maxSize { maxSize } else { calculateSize(proposal) }
|
||||
|
|
|
@ -14,7 +14,8 @@ struct StatusRowTagView: View {
|
|||
if isHomeTimeline,
|
||||
let tag = viewModel.finalStatus.content.links.first(where: { link in
|
||||
link.type == .hashtag && currentAccount.tags.contains(where: { $0.name.lowercased() == link.title.lowercased() })
|
||||
}) {
|
||||
})
|
||||
{
|
||||
Text("#\(tag.title)")
|
||||
.font(.scaledFootnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
|
|
@ -30,7 +30,6 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
|
|||
}
|
||||
|
||||
public enum TimelineFilter: Hashable, Equatable, Identifiable {
|
||||
|
||||
case home, local, federated, trending
|
||||
case hashtag(tag: String, accountId: String?)
|
||||
case tagGroup(title: String, tags: [String], symbolName: String?)
|
||||
|
@ -50,7 +49,6 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable {
|
|||
default:
|
||||
return title
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
@MainActor
|
||||
@Observable class TimelineUnreadStatusesObserver {
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct TimelineContentFilterView: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(CurrentInstance.self) private var currentInstance
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
@State private var contentFilter = TimelineContentFilter.shared
|
||||
@State private var isEditingFilters = false
|
||||
|
||||
public init() {}
|
||||
|
||||
|
@ -27,6 +32,22 @@ public struct TimelineContentFilterView: View {
|
|||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section {
|
||||
if currentInstance.isFiltersSupported {
|
||||
Button {
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
} label: {
|
||||
Label("account.action.edit-filters", systemImage: "line.3.horizontal.decrease.circle")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("timeline.content-filter.title")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineHeaderView<Content: View>: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import DesignSystem
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct TimelineQuickAccessPills: View {
|
||||
|
@ -25,14 +25,15 @@ struct TimelineQuickAccessPills: View {
|
|||
}
|
||||
.scrollClipDisabled()
|
||||
.scrollIndicators(.never)
|
||||
.onChange(of: currentAccount.lists, { _, lists in
|
||||
.onChange(of: currentAccount.lists) { _, lists in
|
||||
guard client.isAuth else { return }
|
||||
var filters = pinnedFilters
|
||||
for (index, filter) in filters.enumerated() {
|
||||
switch filter {
|
||||
case .list(let list):
|
||||
case let .list(list):
|
||||
if let accountList = lists.first(where: { $0.id == list.id }),
|
||||
accountList.title != list.title {
|
||||
accountList.title != list.title
|
||||
{
|
||||
filters[index] = .list(list: accountList)
|
||||
}
|
||||
default:
|
||||
|
@ -40,7 +41,7 @@ struct TimelineQuickAccessPills: View {
|
|||
}
|
||||
}
|
||||
pinnedFilters = filters
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -87,7 +88,7 @@ struct TimelineQuickAccessPills: View {
|
|||
|
||||
private func isFilterSupported(_ filter: TimelineFilter) -> Bool {
|
||||
switch filter {
|
||||
case .list(let list):
|
||||
case let .list(list):
|
||||
return currentAccount.lists.contains(where: { $0.id == list.id })
|
||||
default:
|
||||
return true
|
||||
|
@ -100,23 +101,23 @@ struct PillDropDelegate: DropDelegate {
|
|||
@Binding var items: [TimelineFilter]
|
||||
@Binding var draggedItem: TimelineFilter?
|
||||
|
||||
func dropUpdated(info: DropInfo) -> DropProposal? {
|
||||
func dropUpdated(info _: DropInfo) -> DropProposal? {
|
||||
return DropProposal(operation: .move)
|
||||
}
|
||||
|
||||
func performDrop(info: DropInfo) -> Bool {
|
||||
func performDrop(info _: DropInfo) -> Bool {
|
||||
draggedItem = nil
|
||||
return true
|
||||
}
|
||||
|
||||
func dropEntered(info: DropInfo) {
|
||||
func dropEntered(info _: DropInfo) {
|
||||
if let draggedItem {
|
||||
let fromIndex = items.firstIndex(of: draggedItem)
|
||||
if let fromIndex {
|
||||
let toIndex = items.firstIndex(of: destinationItem)
|
||||
if let toIndex, fromIndex != toIndex {
|
||||
withAnimation {
|
||||
self.items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: (toIndex > fromIndex ? (toIndex + 1) : toIndex))
|
||||
self.items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? (toIndex + 1) : toIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineTagGroupheaderView: View {
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineTagHeaderView: View {
|
||||
@Environment(CurrentAccount.self) private var account
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue