mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 23:50:36 +00:00
Swiftformat
This commit is contained in:
parent
9fd1b4ef73
commit
eb6050a38f
48 changed files with 304 additions and 314 deletions
|
@ -64,7 +64,7 @@ struct AboutView: View {
|
|||
• [KeychainSwift](https://github.com/evgenyneu/keychain-swift)
|
||||
|
||||
• [LRUCache](https://github.com/nicklockwood/LRUCache)
|
||||
|
||||
|
||||
• [Boutique](https://github.com/mergesort/Boutique)
|
||||
|
||||
• [Nuke](https://github.com/kean/Nuke)
|
||||
|
|
|
@ -3,9 +3,9 @@ import AppAccount
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import Network
|
||||
|
||||
struct AccountSettingsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
|
|
@ -38,12 +38,12 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
||||
Section("settings.display.section.display") {
|
||||
Picker("settings.display.font", selection: .init(get: { () -> FontState in
|
||||
if userPreferences.chosenFont?.fontName == "OpenDyslexic-Regular" {
|
||||
return FontState.openDyslexic
|
||||
} else if userPreferences.chosenFont?.fontName == "AtkinsonHyperlegible-Regular" {
|
||||
} else if userPreferences.chosenFont?.fontName == "AtkinsonHyperlegible-Regular" {
|
||||
return FontState.hyperLegible
|
||||
}
|
||||
return userPreferences.chosenFontData != nil ? FontState.custom : FontState.system
|
||||
|
@ -79,7 +79,7 @@ struct DisplaySettingsView: View {
|
|||
Text(buttonStyle.description).tag(buttonStyle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Picker("settings.display.status.media-style", selection: $theme.statusDisplayStyle) {
|
||||
ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in
|
||||
Text(buttonStyle.description).tag(buttonStyle)
|
||||
|
|
|
@ -45,8 +45,8 @@ struct IconSelectorView: View {
|
|||
|
||||
static let items = [
|
||||
IconSelector(title: "Official icons", icons: [.primary, .alt1, .alt2, .alt3, .alt4, .alt5, .alt6, .alt7, .alt8,
|
||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14,
|
||||
.alt15, .alt16, .alt17, .alt18, .alt19, .alt25]),
|
||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14,
|
||||
.alt15, .alt16, .alt17, .alt18, .alt19, .alt25]),
|
||||
IconSelector(title: "Icons by Albert Kinng", icons: [.alt20, .alt21, .alt22, .alt23, .alt24]),
|
||||
IconSelector(title: "Icons by Dan van Moll", icons: [.alt26, .alt27, .alt28]),
|
||||
IconSelector(title: "Icons by @te6-in (GitHub)", icons: [.alt29, .alt30, .alt31, .alt32]),
|
||||
|
|
|
@ -102,11 +102,11 @@ struct SettingsTabs: View {
|
|||
NavigationLink(destination: DisplaySettingsView()) {
|
||||
Label("settings.general.display", systemImage: "paintpalette")
|
||||
}
|
||||
if HapticManager.shared.supportsHaptics {
|
||||
NavigationLink(destination: HapticSettingsView()) {
|
||||
Label("settings.general.haptic", systemImage: "waveform.path")
|
||||
}
|
||||
if HapticManager.shared.supportsHaptics {
|
||||
NavigationLink(destination: HapticSettingsView()) {
|
||||
Label("settings.general.haptic", systemImage: "waveform.path")
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: remoteLocalTimelinesView) {
|
||||
Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import SwiftUI
|
|||
struct SwipeActionsSettingsView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var userPreferences: UserPreferences
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("settings.swipeactions.status") {
|
||||
|
@ -16,7 +16,7 @@ struct SwipeActionsSettingsView: View {
|
|||
}
|
||||
Section {
|
||||
ForEach(StatusAction.allCases) { action in
|
||||
if (action != .none) {
|
||||
if action != .none {
|
||||
Label(action.displayName(), systemImage: action.iconName()).tag(action)
|
||||
}
|
||||
}
|
||||
|
@ -28,20 +28,20 @@ struct SwipeActionsSettingsView: View {
|
|||
}
|
||||
Section {
|
||||
ForEach(StatusAction.allCases) { action in
|
||||
if (action != .none) {
|
||||
if action != .none {
|
||||
Label(action.displayName(), systemImage: action.iconName()).tag(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Label("settings.swipeactions.status.trailing", systemImage: "arrow.left.circle")
|
||||
Picker(selection: $userPreferences.swipeActionsStatusTrailingLeft, label: makeSwipeLabel(left: true, text: "settings.swipeactions.status.trailing.left")) {
|
||||
Picker(selection: $userPreferences.swipeActionsStatusTrailingLeft, label: makeSwipeLabel(left: true, text: "settings.swipeactions.status.trailing.left")) {
|
||||
Section {
|
||||
Label(StatusAction.none.displayName(), systemImage: StatusAction.none.iconName()).tag(StatusAction.none)
|
||||
}
|
||||
Section {
|
||||
ForEach(StatusAction.allCases) { action in
|
||||
if (action != .none) {
|
||||
if action != .none {
|
||||
Label(action.displayName(), systemImage: action.iconName()).tag(action)
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ struct SwipeActionsSettingsView: View {
|
|||
}
|
||||
Section {
|
||||
ForEach(StatusAction.allCases) { action in
|
||||
if (action != .none) {
|
||||
if action != .none {
|
||||
Label(action.displayName(), systemImage: action.iconName()).tag(action)
|
||||
}
|
||||
}
|
||||
|
@ -66,9 +66,9 @@ struct SwipeActionsSettingsView: View {
|
|||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
}
|
||||
|
||||
|
||||
private func makeSwipeLabel(left: Bool, text: LocalizedStringKey) -> some View {
|
||||
return Label(text, systemImage: left ? "rectangle.lefthalf.filled" : "rectangle.righthalf.filled")
|
||||
.padding(.leading, 16)
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ struct TimelineTab: View {
|
|||
@State private var didAppear: Bool = false
|
||||
@State private var timeline: TimelineFilter
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
|
||||
@AppStorage("last_timeline_filter") public var lastTimelineFilter: TimelineFilter = TimelineFilter.home
|
||||
|
||||
@AppStorage("last_timeline_filter") public var lastTimelineFilter: TimelineFilter = .home
|
||||
|
||||
private let canFilterTimeline: Bool
|
||||
|
||||
|
@ -45,10 +45,10 @@ struct TimelineTab: View {
|
|||
routerPath.client = client
|
||||
if !didAppear && canFilterTimeline {
|
||||
didAppear = true
|
||||
if(client.isAuth) {
|
||||
if client.isAuth {
|
||||
timeline = lastTimelineFilter
|
||||
} else {
|
||||
timeline = .federated
|
||||
timeline = .federated
|
||||
}
|
||||
}
|
||||
Task {
|
||||
|
@ -58,18 +58,18 @@ struct TimelineTab: View {
|
|||
routerPath.presentedSheet = .addAccount
|
||||
}
|
||||
}
|
||||
.onChange(of: client.isAuth, perform: { isAuth in
|
||||
if(client.isAuth) {
|
||||
.onChange(of: client.isAuth, perform: { _ in
|
||||
if client.isAuth {
|
||||
timeline = lastTimelineFilter
|
||||
} else {
|
||||
timeline = .federated
|
||||
timeline = .federated
|
||||
}
|
||||
})
|
||||
.onChange(of: currentAccount.account?.id, perform: { _ in
|
||||
if(client.isAuth && canFilterTimeline) {
|
||||
if client.isAuth, canFilterTimeline {
|
||||
timeline = lastTimelineFilter
|
||||
} else {
|
||||
timeline = .federated
|
||||
timeline = .federated
|
||||
}
|
||||
})
|
||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
||||
|
@ -85,7 +85,7 @@ struct TimelineTab: View {
|
|||
routerPath.path = []
|
||||
}
|
||||
.onChange(of: timeline) { timeline in
|
||||
if(timeline == .home || timeline == .federated || timeline == .local) {
|
||||
if timeline == .home || timeline == .federated || timeline == .local {
|
||||
lastTimelineFilter = timeline
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ struct AccountDetailHeaderView: View {
|
|||
enum Constants {
|
||||
static let headerHeight: CGFloat = 200
|
||||
}
|
||||
|
||||
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var quickLook: QuickLook
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
|
@ -100,7 +100,7 @@ struct AccountDetailHeaderView: View {
|
|||
makeCustomInfoLabel(title: "account.posts", count: account.statusesCount)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
||||
|
||||
Button {
|
||||
routerPath.navigate(to: .following(id: account.id))
|
||||
} label: {
|
||||
|
@ -118,7 +118,7 @@ struct AccountDetailHeaderView: View {
|
|||
)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
||||
|
||||
}.offset(y: 20)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ public struct AccountDetailView: View {
|
|||
public init(account: Account) {
|
||||
_viewModel = StateObject(wrappedValue: .init(account: account))
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
|
@ -48,7 +48,7 @@ public struct AccountDetailView: View {
|
|||
.listRowInsets(.init())
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
||||
Picker("", selection: $viewModel.selectedTab) {
|
||||
ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs,
|
||||
id: \.self) { tab in
|
||||
|
@ -283,8 +283,8 @@ public struct AccountDetailView: View {
|
|||
ForEach(currentAccount.sortedLists) { list in
|
||||
NavigationLink(value: RouterDestinations.list(list: list)) {
|
||||
Text(list.title)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.contextMenu {
|
||||
|
@ -389,7 +389,7 @@ public struct AccountDetailView: View {
|
|||
Label("account.action.block", systemImage: "person.crop.circle.badge.xmark")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if viewModel.relationship?.muting == true {
|
||||
Button {
|
||||
Task {
|
||||
|
@ -405,7 +405,7 @@ public struct AccountDetailView: View {
|
|||
} else {
|
||||
Menu {
|
||||
ForEach(MutingDurations.allCases, id: \.rawValue) { duration in
|
||||
Button (duration.description) {
|
||||
Button(duration.description) {
|
||||
Task {
|
||||
do {
|
||||
viewModel.relationship = try await client.post(endpoint: Accounts.mute(id: account.id, json: MuteData(duration: duration.rawValue)))
|
||||
|
|
|
@ -3,13 +3,13 @@ import SwiftUI
|
|||
enum MutingDurations: Int, CaseIterable {
|
||||
case infinite = 0
|
||||
case fiveMinutes = 300
|
||||
case thirtyMinutes = 1_800
|
||||
case oneHour = 3_600
|
||||
case sixHours = 21_600
|
||||
case oneDay = 86_400
|
||||
case thirtyMinutes = 1800
|
||||
case oneHour = 3600
|
||||
case sixHours = 21600
|
||||
case oneDay = 86400
|
||||
case threeDays = 259_200
|
||||
case sevenDays = 604_800
|
||||
|
||||
|
||||
public var description: LocalizedStringKey {
|
||||
switch self {
|
||||
case .infinite:
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import DesignSystem
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
@MainActor
|
||||
public class AppAccountViewModel: ObservableObject {
|
||||
private static var avatarsCache: [String: UIImage] = [:]
|
||||
|
||||
|
||||
var appAccount: AppAccount
|
||||
let client: Client
|
||||
let isCompact: Bool
|
||||
|
||||
@Published var account: Account?
|
||||
@Published var roundedAvatar: UIImage?
|
||||
|
||||
|
||||
var acct: String {
|
||||
if let acct = appAccount.accountName {
|
||||
return acct
|
||||
|
@ -35,17 +35,18 @@ public class AppAccountViewModel: ObservableObject {
|
|||
appAccount.accountName = "\(account.acct)@\(appAccount.server)"
|
||||
try appAccount.save()
|
||||
}
|
||||
|
||||
|
||||
if let account {
|
||||
if let image = Self.avatarsCache[account.id] {
|
||||
self.roundedAvatar = image
|
||||
roundedAvatar = image
|
||||
} else if let (data, _) = try? await URLSession.shared.data(from: account.avatar),
|
||||
let image = UIImage(data: data)?.roundedImage {
|
||||
self.roundedAvatar = image
|
||||
let image = UIImage(data: data)?.roundedImage
|
||||
{
|
||||
roundedAvatar = image
|
||||
Self.avatarsCache[account.id] = image
|
||||
}
|
||||
}
|
||||
|
||||
} catch { }
|
||||
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class ConversationDetailViewModel: ObservableObject {
|
|||
|
||||
init(conversation: Conversation) {
|
||||
self.conversation = conversation
|
||||
messages = conversation.lastStatus != nil ? [conversation.lastStatus!] : []
|
||||
messages = conversation.lastStatus != nil ? [conversation.lastStatus!] : []
|
||||
}
|
||||
|
||||
func fetchMessages() async {
|
||||
|
|
|
@ -60,8 +60,8 @@ struct ConversationMessageView: View {
|
|||
.padding(.leading, isOwnMessage ? 24 : 0)
|
||||
.padding(.trailing, isOwnMessage ? 0 : 24)
|
||||
}
|
||||
|
||||
if message.id == String(conversation.lastStatus?.id ?? "") {
|
||||
|
||||
if message.id == String(conversation.lastStatus?.id ?? "") {
|
||||
HStack {
|
||||
if isOwnMessage {
|
||||
Spacer()
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import UIKit
|
||||
|
||||
extension UIImage{
|
||||
public var roundedImage: UIImage? {
|
||||
let rect = CGRect(origin:CGPoint(x: 0, y: 0), size: self.size)
|
||||
UIGraphicsBeginImageContextWithOptions(self.size, false, 1)
|
||||
public extension UIImage {
|
||||
var roundedImage: UIImage? {
|
||||
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 1)
|
||||
defer { UIGraphicsEndImageContext() }
|
||||
UIBezierPath(
|
||||
roundedRect: rect,
|
||||
cornerRadius: self.size.height
|
||||
cornerRadius: size.height
|
||||
).addClip()
|
||||
self.draw(in: rect)
|
||||
draw(in: rect)
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ public class CurrentInstance: ObservableObject {
|
|||
private var client: Client?
|
||||
|
||||
public static let shared = CurrentInstance()
|
||||
|
||||
|
||||
private var version: Float {
|
||||
if let stringVersion = instance?.version {
|
||||
if stringVersion.utf8.count > 2 {
|
||||
|
@ -20,7 +20,6 @@ public class CurrentInstance: ObservableObject {
|
|||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
public var isFiltersSupported: Bool {
|
||||
version >= 4
|
||||
|
@ -29,7 +28,7 @@ public class CurrentInstance: ObservableObject {
|
|||
public var isEditSupported: Bool {
|
||||
version >= 4
|
||||
}
|
||||
|
||||
|
||||
public var isEditAltTextSupported: Bool {
|
||||
version >= 4.1
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import SwiftUI
|
||||
|
||||
public enum StatusAction : String, CaseIterable, Identifiable {
|
||||
public enum StatusAction: String, CaseIterable, Identifiable {
|
||||
public var id: String {
|
||||
"\(rawValue)"
|
||||
}
|
||||
|
||||
|
||||
case none, reply, boost, favorite, bookmark, quote
|
||||
|
||||
|
||||
public func displayName(isReblogged: Bool = false, isFavorited: Bool = false, isBookmarked: Bool = false) -> LocalizedStringKey {
|
||||
switch self {
|
||||
case .none:
|
||||
|
@ -23,7 +23,7 @@ public enum StatusAction : String, CaseIterable, Identifiable {
|
|||
return isBookmarked ? "status.action.unbookmark" : "settings.swipeactions.status.action.bookmark"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func iconName(isReblogged: Bool = false, isFavorited: Bool = false, isBookmarked: Bool = false) -> String {
|
||||
switch self {
|
||||
case .none:
|
||||
|
@ -40,7 +40,7 @@ public enum StatusAction : String, CaseIterable, Identifiable {
|
|||
return isBookmarked ? "bookmark.fill" : "bookmark"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func color(themeTintColor: Color) -> Color {
|
||||
switch self {
|
||||
case .none:
|
||||
|
|
|
@ -31,13 +31,13 @@ public class UserPreferences: ObservableObject {
|
|||
@AppStorage("suppress_dupe_reblogs") public var suppressDupeReblogs: Bool = false
|
||||
|
||||
@AppStorage("inAppBrowserReaderView") public var inAppBrowserReaderView = false
|
||||
|
||||
|
||||
@AppStorage("haptic_tab") public var hapticTabSelectionEnabled = true
|
||||
@AppStorage("haptic_timeline") public var hapticTimelineEnabled = true
|
||||
@AppStorage("haptic_button_press") public var hapticButtonPressEnabled = true
|
||||
|
||||
|
||||
@AppStorage("show_tab_label_iphone") public var showiPhoneTabLabel = true
|
||||
|
||||
|
||||
@AppStorage("show_second_column_ipad") public var showiPadSecondaryColumn = true
|
||||
|
||||
@AppStorage("swipeactions-status-trailing-right") public var swipeActionsStatusTrailingRight = StatusAction.favorite
|
||||
|
|
|
@ -2,7 +2,7 @@ import Foundation
|
|||
import SwiftSoup
|
||||
import SwiftUI
|
||||
|
||||
fileprivate enum CodingKeys: CodingKey {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case htmlValue, asMarkdown, asRawText, statusesURLs
|
||||
}
|
||||
|
||||
|
@ -11,12 +11,12 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
public var asMarkdown: String = ""
|
||||
public var asRawText: String = ""
|
||||
public var statusesURLs = [URL]()
|
||||
|
||||
|
||||
public var asSafeMarkdownAttributedString: AttributedString = .init()
|
||||
private var main_regex: NSRegularExpression?
|
||||
private var underscore_regex: NSRegularExpression?
|
||||
public init(from decoder: Decoder) {
|
||||
var alreadyDecoded: Bool = false
|
||||
var alreadyDecoded = false
|
||||
do {
|
||||
let container = try decoder.singleValueContainer()
|
||||
htmlValue = try container.decode(String.self)
|
||||
|
@ -67,7 +67,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
asRawText = htmlValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true,
|
||||
interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||
|
@ -77,12 +77,12 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
public init(stringValue: String, parseMarkdown:Bool = false) {
|
||||
public init(stringValue: String, parseMarkdown: Bool = false) {
|
||||
htmlValue = stringValue
|
||||
asMarkdown = stringValue
|
||||
asRawText = stringValue
|
||||
statusesURLs = []
|
||||
|
||||
|
||||
if parseMarkdown {
|
||||
do {
|
||||
let options = AttributedString.MarkdownParsingOptions(allowsExtendedAttributes: true,
|
||||
|
@ -91,8 +91,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
} catch {
|
||||
asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Foundation
|
||||
|
||||
fileprivate enum CodingKeys: CodingKey {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case asDate
|
||||
}
|
||||
|
||||
public struct ServerDate: Codable, Hashable, Equatable {
|
||||
public let asDate: Date
|
||||
|
||||
|
||||
public var relativeFormatted: String {
|
||||
Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date())
|
||||
}
|
||||
|
@ -14,9 +14,9 @@ public struct ServerDate: Codable, Hashable, Equatable {
|
|||
public var shortDateFormatted: String {
|
||||
Self.createdAtShortDateFormatted.string(from: asDate)
|
||||
}
|
||||
|
||||
|
||||
private static let calendar = Calendar(identifier: .gregorian)
|
||||
|
||||
|
||||
private static var createdAtDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.calendar = .init(identifier: .iso8601)
|
||||
|
@ -24,24 +24,24 @@ public struct ServerDate: Codable, Hashable, Equatable {
|
|||
dateFormatter.timeZone = .init(abbreviation: "UTC")
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
private static var createdAtRelativeFormatter: RelativeDateTimeFormatter = {
|
||||
let dateFormatter = RelativeDateTimeFormatter()
|
||||
dateFormatter.unitsStyle = .short
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
private static var createdAtShortDateFormatted: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
public init() {
|
||||
asDate = Date()-100
|
||||
asDate = Date() - 100
|
||||
}
|
||||
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
do {
|
||||
// Decode from server
|
||||
|
@ -54,7 +54,7 @@ public struct ServerDate: Codable, Hashable, Equatable {
|
|||
asDate = try container.decode(Date.self, forKey: .asDate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(asDate, forKey: .asDate)
|
||||
|
|
|
@ -54,14 +54,12 @@ public protocol AnyStatus {
|
|||
var language: String? { get }
|
||||
}
|
||||
|
||||
extension AnyStatus {
|
||||
public var viewId: String {
|
||||
get {
|
||||
if let editedAt {
|
||||
return "\(id)\(editedAt.asDate.description)"
|
||||
}
|
||||
return id
|
||||
public extension AnyStatus {
|
||||
var viewId: String {
|
||||
if let editedAt {
|
||||
return "\(id)\(editedAt.asDate.description)"
|
||||
}
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,41 +105,40 @@ public struct Status: AnyStatus, Codable, Identifiable, Equatable, Hashable, Sta
|
|||
public let sensitive: Bool
|
||||
public let language: String?
|
||||
|
||||
public static func placeholder(forSettings:Bool = false, language:String? = nil) -> Status {
|
||||
public static func placeholder(forSettings: Bool = false, language: String? = nil) -> Status {
|
||||
.init(id: UUID().uuidString,
|
||||
content: .init(stringValue: "Lorem ipsum [#dolor](#) sit amet\nconsectetur [@adipiscing](#) elit\nAsed do eiusmod tempor incididunt ut labore.", parseMarkdown: forSettings),
|
||||
content: .init(stringValue: "Lorem ipsum [#dolor](#) sit amet\nconsectetur [@adipiscing](#) elit\nAsed do eiusmod tempor incididunt ut labore.", parseMarkdown: forSettings),
|
||||
|
||||
account: .placeholder(),
|
||||
createdAt: ServerDate(),
|
||||
editedAt: nil,
|
||||
reblog: nil,
|
||||
mediaAttachments: [],
|
||||
mentions: [],
|
||||
repliesCount: 0,
|
||||
reblogsCount: 0,
|
||||
favouritesCount: 0,
|
||||
card: nil,
|
||||
favourited: false,
|
||||
reblogged: false,
|
||||
pinned: false,
|
||||
bookmarked: false,
|
||||
emojis: [],
|
||||
url: "https://example.com",
|
||||
application: nil,
|
||||
inReplyToAccountId: nil,
|
||||
visibility: .pub,
|
||||
poll: nil,
|
||||
spoilerText: .init(stringValue: ""),
|
||||
filtered: [],
|
||||
sensitive: false,
|
||||
language: language)
|
||||
account: .placeholder(),
|
||||
createdAt: ServerDate(),
|
||||
editedAt: nil,
|
||||
reblog: nil,
|
||||
mediaAttachments: [],
|
||||
mentions: [],
|
||||
repliesCount: 0,
|
||||
reblogsCount: 0,
|
||||
favouritesCount: 0,
|
||||
card: nil,
|
||||
favourited: false,
|
||||
reblogged: false,
|
||||
pinned: false,
|
||||
bookmarked: false,
|
||||
emojis: [],
|
||||
url: "https://example.com",
|
||||
application: nil,
|
||||
inReplyToAccountId: nil,
|
||||
visibility: .pub,
|
||||
poll: nil,
|
||||
spoilerText: .init(stringValue: ""),
|
||||
filtered: [],
|
||||
sensitive: false,
|
||||
language: language)
|
||||
}
|
||||
|
||||
|
||||
public static func placeholders() -> [Status] {
|
||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
||||
}
|
||||
|
||||
|
||||
public var reblogAsAsStatus: Status? {
|
||||
if let reblog {
|
||||
return .init(id: reblog.id,
|
||||
|
|
|
@ -3,7 +3,7 @@ import Foundation
|
|||
public struct StatusContext: Decodable {
|
||||
public let ancestors: [Status]
|
||||
public let descendants: [Status]
|
||||
|
||||
|
||||
public static func empty() -> StatusContext {
|
||||
.init(ancestors: [], descendants: [])
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ public struct StatusTranslation: Decodable {
|
|||
public let content: HTMLString
|
||||
public let detectedSourceLanguage: String
|
||||
public let provider: String
|
||||
|
||||
|
||||
public init(content: String, detectedSourceLanguage: String, provider: String) {
|
||||
self.content = .init(stringValue: content)
|
||||
self.detectedSourceLanguage = detectedSourceLanguage
|
||||
|
|
|
@ -5,7 +5,7 @@ public struct DeepLClient {
|
|||
public enum DeepLError: Error {
|
||||
case notFound
|
||||
}
|
||||
|
||||
|
||||
private let endpoint = "https://api.deepl.com/v2/translate"
|
||||
|
||||
private var APIKey: String {
|
||||
|
|
|
@ -34,7 +34,7 @@ public enum Accounts: Endpoint {
|
|||
case unblock(id: String)
|
||||
case mute(id: String, json: MuteData)
|
||||
case unmute(id: String)
|
||||
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
case let .accounts(id):
|
||||
|
@ -81,7 +81,7 @@ public enum Accounts: Endpoint {
|
|||
return "accounts/\(id)/unmute"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
switch self {
|
||||
case let .statuses(_, sinceId, tag, onlyMedia, excludeReplies, pinned):
|
||||
|
@ -138,7 +138,7 @@ public enum Accounts: Endpoint {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var jsonValue: Encodable? {
|
||||
switch self {
|
||||
case let .mute(_, json):
|
||||
|
@ -151,7 +151,7 @@ public enum Accounts: Endpoint {
|
|||
|
||||
public struct MuteData: Encodable {
|
||||
public let duration: Int
|
||||
|
||||
|
||||
public init(duration: Int) {
|
||||
self.duration = duration
|
||||
}
|
||||
|
|
|
@ -105,14 +105,14 @@ public struct StatusData: Encodable {
|
|||
self.expires_in = expires_in
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct MediaAttribute: Encodable {
|
||||
public let id: String
|
||||
public let description: String?
|
||||
public let thumbnail: String?
|
||||
public let focus: String?
|
||||
|
||||
public init(id: String, description: String?, thumbnail: String?, focus: String?) {
|
||||
|
||||
public init(id: String, description: String?, thumbnail: String?, focus: String?) {
|
||||
self.id = id
|
||||
self.description = description
|
||||
self.thumbnail = thumbnail
|
||||
|
|
|
@ -98,8 +98,8 @@ public struct NotificationsListView: View {
|
|||
EmptyView(iconName: "bell.slash",
|
||||
title: "notifications.empty.title",
|
||||
message: "notifications.empty.message")
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listSectionSeparator(.hidden)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listSectionSeparator(.hidden)
|
||||
} else {
|
||||
ForEach(notifications) { notification in
|
||||
NotificationRowView(notification: notification)
|
||||
|
|
|
@ -15,7 +15,7 @@ class NotificationsViewModel: ObservableObject {
|
|||
case display(notifications: [ConsolidatedNotification], nextPageState: State.PagingState)
|
||||
case error(error: Error)
|
||||
}
|
||||
|
||||
|
||||
enum Constants {
|
||||
static let notificationLimit: Int = 30
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ class NotificationsViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentAccount: CurrentAccount?
|
||||
|
||||
@Published var state: State = .loading
|
||||
|
@ -92,7 +93,7 @@ class NotificationsViewModel: ObservableObject {
|
|||
state = .error(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func fetchNewPages(minId: String, maxPages: Int) async -> [Models.Notification] {
|
||||
guard let client else { return [] }
|
||||
var pagesLoaded = 0
|
||||
|
@ -100,10 +101,10 @@ class NotificationsViewModel: ObservableObject {
|
|||
var latestMinId = minId
|
||||
do {
|
||||
while let newNotifications: [Models.Notification] =
|
||||
try await client.get(endpoint: Notifications.notifications(minId: latestMinId,
|
||||
maxId: nil,
|
||||
types: queryTypes,
|
||||
limit: Constants.notificationLimit)),
|
||||
try await client.get(endpoint: Notifications.notifications(minId: latestMinId,
|
||||
maxId: nil,
|
||||
types: queryTypes,
|
||||
limit: Constants.notificationLimit)),
|
||||
!newNotifications.isEmpty,
|
||||
pagesLoaded < maxPages
|
||||
{
|
||||
|
|
|
@ -4,7 +4,6 @@ import Models
|
|||
import Network
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
public struct StatusDetailView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
@ -16,19 +15,19 @@ public struct StatusDetailView: View {
|
|||
@StateObject private var viewModel: StatusDetailViewModel
|
||||
@State private var isLoaded: Bool = false
|
||||
@State private var statusHeight: CGFloat = 0
|
||||
|
||||
|
||||
public init(statusId: String) {
|
||||
_viewModel = StateObject(wrappedValue: .init(statusId: statusId))
|
||||
}
|
||||
|
||||
|
||||
public init(status: Status) {
|
||||
_viewModel = StateObject(wrappedValue: .init(status: status))
|
||||
}
|
||||
|
||||
|
||||
public init(remoteStatusURL: URL) {
|
||||
_viewModel = StateObject(wrappedValue: .init(remoteStatusURL: remoteStatusURL))
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
GeometryReader { reader in
|
||||
ScrollViewReader { proxy in
|
||||
|
@ -36,38 +35,38 @@ public struct StatusDetailView: View {
|
|||
if isLoaded {
|
||||
topPaddingView
|
||||
}
|
||||
|
||||
|
||||
switch viewModel.state {
|
||||
case .loading:
|
||||
loadingDetailView
|
||||
|
||||
|
||||
case let .display(status, context, date):
|
||||
if !context.ancestors.isEmpty {
|
||||
ForEach(context.ancestors) { ancestor in
|
||||
StatusRowView(viewModel: .init(status: ancestor, isCompact: false))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
makeCurrentStatusView(status: status)
|
||||
.id(date)
|
||||
|
||||
|
||||
if !context.descendants.isEmpty {
|
||||
ForEach(context.descendants) { descendant in
|
||||
StatusRowView(viewModel: .init(status: descendant, isCompact: false))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !isLoaded {
|
||||
loadingContextView
|
||||
}
|
||||
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(theme.secondaryBackgroundColor)
|
||||
.frame(minHeight: reader.frame(in: .local).size.height - statusHeight)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowInsets(.init())
|
||||
|
||||
|
||||
case .error:
|
||||
errorView
|
||||
}
|
||||
|
@ -87,7 +86,7 @@ public struct StatusDetailView: View {
|
|||
viewModel.client = client
|
||||
let result = await viewModel.fetch()
|
||||
isLoaded = true
|
||||
|
||||
|
||||
if !result {
|
||||
if let url = viewModel.remoteStatusURL {
|
||||
openURL(url)
|
||||
|
@ -106,22 +105,22 @@ public struct StatusDetailView: View {
|
|||
.navigationTitle(viewModel.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
|
||||
private func makeCurrentStatusView(status: Status) -> some View {
|
||||
StatusRowView(viewModel: .init(status: status,
|
||||
isCompact: false,
|
||||
isFocused: true))
|
||||
.overlay {
|
||||
GeometryReader { reader in
|
||||
VStack{}
|
||||
.onAppear {
|
||||
statusHeight = reader.size.height
|
||||
.overlay {
|
||||
GeometryReader { reader in
|
||||
VStack {}
|
||||
.onAppear {
|
||||
statusHeight = reader.size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.id(status.id)
|
||||
.id(status.id)
|
||||
}
|
||||
|
||||
|
||||
private var errorView: some View {
|
||||
ErrorView(title: "status.error.title",
|
||||
message: "status.error.message",
|
||||
|
@ -133,14 +132,14 @@ public struct StatusDetailView: View {
|
|||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
|
||||
|
||||
private var loadingDetailView: some View {
|
||||
ForEach(Status.placeholders()) { status in
|
||||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var loadingContextView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
@ -152,7 +151,7 @@ public struct StatusDetailView: View {
|
|||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowInsets(.init())
|
||||
}
|
||||
|
||||
|
||||
private var topPaddingView: some View {
|
||||
HStack { EmptyView() }
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
|
|
@ -23,7 +23,7 @@ class StatusDetailViewModel: ObservableObject {
|
|||
self.statusId = statusId
|
||||
remoteStatusURL = nil
|
||||
}
|
||||
|
||||
|
||||
init(status: Status) {
|
||||
state = .display(status: status, context: .empty(), date: Date())
|
||||
title = "status.post-from-\(status.account.displayNameWithoutEmojis)"
|
||||
|
@ -78,7 +78,7 @@ class StatusDetailViewModel: ObservableObject {
|
|||
state = .display(status: data.status, context: data.context, date: Date())
|
||||
}
|
||||
} else {
|
||||
state = .display(status: data.status, context: data.context,date: Date())
|
||||
state = .display(status: data.status, context: data.context, date: Date())
|
||||
scrollToId = statusId
|
||||
}
|
||||
} catch {
|
||||
|
|
|
@ -33,7 +33,7 @@ struct StatusEditorAccessoryView: View {
|
|||
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||
}
|
||||
}
|
||||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||
.disabled(viewModel.showPoll)
|
||||
|
||||
Button {
|
||||
|
@ -85,7 +85,7 @@ struct StatusEditorAccessoryView: View {
|
|||
}
|
||||
.accessibilityLabel("accessibility.editor.button.language")
|
||||
|
||||
if preferences.isOpenAIEnabled {
|
||||
if preferences.isOpenAIEnabled {
|
||||
AIMenu.disabled(!viewModel.canPost)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isGif: Bool {
|
||||
switch self {
|
||||
case .gif, .gif2:
|
||||
|
@ -54,12 +54,14 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
|||
if self == .jpeg || self == .png || self == .tiff || self == .image {
|
||||
if let imageURL = result as? URL,
|
||||
let data = try? Data(contentsOf: imageURL),
|
||||
let image = UIImage(data: data) {
|
||||
let image = UIImage(data: data)
|
||||
{
|
||||
return image
|
||||
} else if let data = result as? Data,
|
||||
let image = UIImage(data: data) {
|
||||
let image = UIImage(data: data)
|
||||
{
|
||||
return image
|
||||
} else if let transferable = await getImageTansferable(item: item){
|
||||
} else if let transferable = await getImageTansferable(item: item) {
|
||||
return transferable
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +88,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func getGifTransferable(item: NSItemProvider) async -> GifFileTranseferable? {
|
||||
return await withCheckedContinuation { continuation in
|
||||
_ = item.loadTransferable(type: GifFileTranseferable.self) { result in
|
||||
|
@ -99,7 +101,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func getImageTansferable(item: NSItemProvider) async -> ImageFileTranseferable? {
|
||||
return await withCheckedContinuation { continuation in
|
||||
_ = item.loadTransferable(type: ImageFileTranseferable.self) { result in
|
||||
|
@ -139,7 +141,7 @@ struct MovieFileTranseferable: Transferable {
|
|||
FileRepresentation(contentType: .movie) { movie in
|
||||
SentTransferredFile(movie.url)
|
||||
} importing: { received in
|
||||
return Self(url: localURLFor(received: received))
|
||||
Self(url: localURLFor(received: received))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +157,7 @@ struct ImageFileTranseferable: Transferable {
|
|||
FileRepresentation(contentType: .image) { image in
|
||||
SentTransferredFile(image.url)
|
||||
} importing: { received in
|
||||
return Self(url: localURLFor(received: received))
|
||||
Self(url: localURLFor(received: received))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,12 +173,12 @@ struct GifFileTranseferable: Transferable {
|
|||
FileRepresentation(contentType: .gif) { gif in
|
||||
SentTransferredFile(gif.url)
|
||||
} importing: { received in
|
||||
return Self(url: localURLFor(received: received))
|
||||
Self(url: localURLFor(received: received))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func localURLFor(received: ReceivedTransferredFile) -> URL {
|
||||
private func localURLFor(received: ReceivedTransferredFile) -> URL {
|
||||
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
|
||||
try? FileManager.default.copyItem(at: received.file, to: copy)
|
||||
return copy
|
||||
|
|
|
@ -37,11 +37,11 @@ public struct StatusEditorView: View {
|
|||
.padding(.horizontal, .layoutPadding)
|
||||
TextView($viewModel.statusText,
|
||||
getTextView: { textView in
|
||||
viewModel.textView = textView
|
||||
})
|
||||
.placeholder(String(localized: "status.editor.text.placeholder"))
|
||||
.setKeyboardType(preferences.isSocialKeyboardEnabled ? .twitter : .default)
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
viewModel.textView = textView
|
||||
})
|
||||
.placeholder(String(localized: "status.editor.text.placeholder"))
|
||||
.setKeyboardType(preferences.isSocialKeyboardEnabled ? .twitter : .default)
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
StatusEditorMediaView(viewModel: viewModel)
|
||||
if let status = viewModel.embeddedStatus {
|
||||
StatusEmbeddedView(status: status)
|
||||
|
|
|
@ -20,7 +20,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
textView?.pasteDelegate = self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var selectedRange: NSRange {
|
||||
get {
|
||||
guard let textView else {
|
||||
|
@ -32,16 +32,14 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
textView?.selectedRange = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var markedTextRange: UITextRange? {
|
||||
get {
|
||||
guard let textView else {
|
||||
return nil
|
||||
}
|
||||
return textView.markedTextRange
|
||||
guard let textView else {
|
||||
return nil
|
||||
}
|
||||
return textView.markedTextRange
|
||||
}
|
||||
|
||||
|
||||
@Published var statusText = NSMutableAttributedString(string: "") {
|
||||
didSet {
|
||||
let range = selectedRange
|
||||
|
@ -372,7 +370,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
mediaAttachment: nil,
|
||||
error: nil))
|
||||
} else if var content = content as? ImageFileTranseferable,
|
||||
let image = content.image {
|
||||
let image = content.image
|
||||
{
|
||||
mediasImages.append(.init(image: image,
|
||||
movieTransferable: nil,
|
||||
gifTransferable: nil,
|
||||
|
@ -507,7 +506,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
for media in selectedMedias {
|
||||
print(media.supportedContentTypes)
|
||||
var file: (any Transferable)?
|
||||
|
||||
|
||||
if file == nil {
|
||||
file = try? await media.loadTransferable(type: GifFileTranseferable.self)
|
||||
}
|
||||
|
@ -682,6 +681,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
// MARK: - Custom emojis
|
||||
|
||||
func fetchCustomEmojis() async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
|
@ -690,7 +690,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
//MARK: - DropDelegate
|
||||
// MARK: - DropDelegate
|
||||
|
||||
extension StatusEditorViewModel: DropDelegate {
|
||||
public func performDrop(info: DropInfo) -> Bool {
|
||||
let item = info.itemProviders(for: StatusEditorUTTypeSupported.types())
|
||||
|
@ -700,17 +701,20 @@ extension StatusEditorViewModel: DropDelegate {
|
|||
}
|
||||
|
||||
// MARK: - UITextPasteDelegate
|
||||
|
||||
extension StatusEditorViewModel: UITextPasteDelegate {
|
||||
public func textPasteConfigurationSupporting(
|
||||
_ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting,
|
||||
transform item: UITextPasteItem) {
|
||||
if !item.itemProvider.registeredContentTypes(conformingTo: .image).isEmpty ||
|
||||
!item.itemProvider.registeredContentTypes(conformingTo: .video).isEmpty ||
|
||||
!item.itemProvider.registeredContentTypes(conformingTo: .gif).isEmpty {
|
||||
processItemsProvider(items: [item.itemProvider])
|
||||
item.setNoResult()
|
||||
} else {
|
||||
item.setDefaultResult()
|
||||
}
|
||||
_: UITextPasteConfigurationSupporting,
|
||||
transform item: UITextPasteItem
|
||||
) {
|
||||
if !item.itemProvider.registeredContentTypes(conformingTo: .image).isEmpty ||
|
||||
!item.itemProvider.registeredContentTypes(conformingTo: .video).isEmpty ||
|
||||
!item.itemProvider.registeredContentTypes(conformingTo: .gif).isEmpty
|
||||
{
|
||||
processItemsProvider(items: [item.itemProvider])
|
||||
item.setNoResult()
|
||||
} else {
|
||||
item.setDefaultResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import SwiftUI
|
||||
|
||||
extension TextView.Representable {
|
||||
final class Coordinator: NSObject, UITextViewDelegate {
|
||||
|
||||
internal let textView: UIKitTextView
|
||||
|
||||
|
||||
private var originalText: NSMutableAttributedString = .init()
|
||||
private var text: Binding<NSMutableAttributedString>
|
||||
private var calculatedHeight: Binding<CGFloat>
|
||||
|
||||
|
||||
var didBecomeFirstResponder = false
|
||||
|
||||
|
||||
var getTextView: ((UITextView) -> Void)?
|
||||
|
||||
|
||||
init(text: Binding<NSMutableAttributedString>,
|
||||
calculatedHeight: Binding<CGFloat>,
|
||||
getTextView: ((UITextView) -> Void)?
|
||||
) {
|
||||
getTextView: ((UITextView) -> Void)?)
|
||||
{
|
||||
textView = UIKitTextView()
|
||||
textView.backgroundColor = .clear
|
||||
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
textView.isScrollEnabled = false
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.textContainerInset = .zero
|
||||
|
||||
|
||||
self.text = text
|
||||
self.calculatedHeight = calculatedHeight
|
||||
self.getTextView = getTextView
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
textView.delegate = self
|
||||
|
||||
|
||||
textView.font = Font.scaledBodyUIFont
|
||||
textView.adjustsFontForContentSizeCategory = true
|
||||
textView.autocapitalizationType = .sentences
|
||||
|
@ -43,46 +42,43 @@ extension TextView.Representable {
|
|||
textView.allowsEditingTextAttributes = false
|
||||
textView.returnKeyType = .default
|
||||
textView.allowsEditingTextAttributes = true
|
||||
|
||||
|
||||
self.getTextView?(textView)
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
|
||||
func textViewDidBeginEditing(_: UITextView) {
|
||||
originalText = text.wrappedValue
|
||||
DispatchQueue.main.async {
|
||||
self.recalculateHeight()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
DispatchQueue.main.async {
|
||||
self.text.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
|
||||
self.recalculateHeight()
|
||||
}
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
|
||||
func textView(_: UITextView, shouldChangeTextIn _: NSRange, replacementText _: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TextView.Representable.Coordinator {
|
||||
|
||||
func update(representable: TextView.Representable) {
|
||||
textView.keyboardType = representable.keyboard
|
||||
recalculateHeight()
|
||||
textView.setNeedsDisplay()
|
||||
}
|
||||
|
||||
|
||||
private func recalculateHeight() {
|
||||
let newSize = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude))
|
||||
guard calculatedHeight.wrappedValue != newSize.height else { return }
|
||||
|
||||
|
||||
DispatchQueue.main.async { // call in next render cycle.
|
||||
self.calculatedHeight.wrappedValue = newSize.height
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import SwiftUI
|
||||
|
||||
public extension TextView {
|
||||
|
||||
/// Specify a placeholder text
|
||||
/// - Parameter placeholder: The placeholder text
|
||||
func placeholder(_ placeholder: String) -> TextView {
|
||||
self.placeholder(placeholder) { $0 }
|
||||
}
|
||||
|
||||
|
||||
/// Specify a placeholder with the specified configuration
|
||||
///
|
||||
/// Example:
|
||||
|
@ -22,18 +21,17 @@ public extension TextView {
|
|||
view.placeholderView = AnyView(configure(text))
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
/// Specify a custom placeholder view
|
||||
func placeholder<V: View>(_ placeholder: V) -> TextView {
|
||||
var view = self
|
||||
view.placeholderView = AnyView(placeholder)
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
func setKeyboardType(_ keyboardType: UIKeyboardType) -> TextView {
|
||||
var view = self
|
||||
view.keyboard = keyboardType
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,25 +2,24 @@ import SwiftUI
|
|||
|
||||
extension TextView {
|
||||
struct Representable: UIViewRepresentable {
|
||||
|
||||
@Binding var text: NSMutableAttributedString
|
||||
@Binding var calculatedHeight: CGFloat
|
||||
|
||||
|
||||
let keyboard: UIKeyboardType
|
||||
var getTextView: ((UITextView) -> Void)?
|
||||
|
||||
|
||||
func makeUIView(context: Context) -> UIKitTextView {
|
||||
context.coordinator.textView
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UIKitTextView, context: Context) {
|
||||
|
||||
func updateUIView(_: UIKitTextView, context: Context) {
|
||||
context.coordinator.update(representable: self)
|
||||
if !context.coordinator.didBecomeFirstResponder {
|
||||
context.coordinator.textView.becomeFirstResponder()
|
||||
context.coordinator.didBecomeFirstResponder = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@discardableResult func makeCoordinator() -> Coordinator {
|
||||
Coordinator(
|
||||
text: $text,
|
||||
|
@ -28,7 +27,5 @@ extension TextView {
|
|||
getTextView: getTextView
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,36 +1,35 @@
|
|||
import SwiftUI
|
||||
import DesignSystem
|
||||
import SwiftUI
|
||||
|
||||
/// A SwiftUI TextView implementation that supports both scrolling and auto-sizing layouts
|
||||
public struct TextView: View {
|
||||
|
||||
@Environment(\.layoutDirection) private var layoutDirection
|
||||
|
||||
|
||||
@Binding private var text: NSMutableAttributedString
|
||||
@Binding private var isEmpty: Bool
|
||||
|
||||
|
||||
@State private var calculatedHeight: CGFloat = 44
|
||||
|
||||
|
||||
private var getTextView: ((UITextView) -> Void)?
|
||||
|
||||
|
||||
var placeholderView: AnyView?
|
||||
var keyboard: UIKeyboardType = .default
|
||||
|
||||
|
||||
/// Makes a new TextView that supports `NSAttributedString`
|
||||
/// - Parameters:
|
||||
/// - text: A binding to the attributed text
|
||||
public init(_ text: Binding<NSMutableAttributedString>,
|
||||
getTextView: ((UITextView) -> Void)? = nil
|
||||
) {
|
||||
getTextView: ((UITextView) -> Void)? = nil)
|
||||
{
|
||||
_text = text
|
||||
_isEmpty = Binding(
|
||||
get: { text.wrappedValue.string.isEmpty },
|
||||
set: { _ in }
|
||||
)
|
||||
|
||||
|
||||
self.getTextView = getTextView
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
Representable(
|
||||
text: $text,
|
||||
|
@ -53,19 +52,16 @@ public struct TextView: View {
|
|||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class UIKitTextView: UITextView {
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return (super.keyCommands ?? []) + [
|
||||
UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escape(_:)))
|
||||
UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escape(_:))),
|
||||
]
|
||||
}
|
||||
|
||||
@objc private func escape(_ sender: Any) {
|
||||
|
||||
@objc private func escape(_: Any) {
|
||||
resignFirstResponder()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,7 +37,8 @@ public struct StatusPollView: View {
|
|||
|
||||
private func isSelected(option: Poll.Option) -> Bool {
|
||||
if let optionIndex = viewModel.poll.options.firstIndex(where: { $0.id == option.id }),
|
||||
let _ = viewModel.votes.firstIndex(of: optionIndex) {
|
||||
let _ = viewModel.votes.firstIndex(of: optionIndex)
|
||||
{
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -62,7 +63,7 @@ public struct StatusPollView: View {
|
|||
return Image(systemName: imageName)
|
||||
.foregroundColor(theme.labelColor)
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(viewModel.poll.options) { option in
|
||||
|
|
|
@ -38,7 +38,7 @@ public class StatusPollViewModel: ObservableObject {
|
|||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func handleSelection(_ pollIndex: Int) {
|
||||
if poll.multiple {
|
||||
if let voterIndex = votes.firstIndex(of: pollIndex) {
|
||||
|
|
|
@ -94,7 +94,7 @@ struct StatusActionsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleAction(action: Actions) {
|
||||
Task {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
|
|
|
@ -163,7 +163,7 @@ public struct StatusMediaPreviewView: View {
|
|||
.frame(width: newSize.width, height: newSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case .gifv, .video, .audio:
|
||||
if let url = attachment.url {
|
||||
VideoPlayerView(viewModel: .init(url: url))
|
||||
|
|
|
@ -83,7 +83,8 @@ struct StatusRowContextMenu: View {
|
|||
}
|
||||
} label: {
|
||||
if let statusLang = viewModel.status.language,
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLang) {
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: statusLang)
|
||||
{
|
||||
Label("status.action.translate-from-\(languageName)", systemImage: "captions.bubble")
|
||||
} else {
|
||||
Label("status.action.translate", systemImage: "captions.bubble")
|
||||
|
@ -113,8 +114,7 @@ struct StatusRowContextMenu: View {
|
|||
}
|
||||
Button(role: .destructive,
|
||||
action: { viewModel.showDeleteAlert = true },
|
||||
label: { Label("status.action.delete", systemImage: "trash") }
|
||||
)
|
||||
label: { Label("status.action.delete", systemImage: "trash") })
|
||||
}
|
||||
} else if !viewModel.isRemote {
|
||||
Section(viewModel.status.account.acct) {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
struct StatusRowDetailView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
|
||||
|
||||
@ObservedObject var viewModel: StatusRowViewModel
|
||||
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Divider()
|
||||
|
@ -84,7 +84,7 @@ struct StatusRowDetailView: View {
|
|||
await viewModel.fetchActionsAccounts()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func makeAccountsScrollView(accounts: [Account]) -> some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 0) {
|
||||
|
@ -96,5 +96,4 @@ struct StatusRowDetailView: View {
|
|||
.padding(.leading, .layoutPadding)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -118,19 +118,21 @@ public struct StatusRowView: View {
|
|||
title: Text("status.action.delete.confirm.title"),
|
||||
message: Text("status.action.delete.confirm.message"),
|
||||
primaryButton: .destructive(
|
||||
Text("status.action.delete")) {
|
||||
Task {
|
||||
await viewModel.delete()
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel())
|
||||
Text("status.action.delete"))
|
||||
{
|
||||
Task {
|
||||
await viewModel.delete()
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
})
|
||||
.alignmentGuide(.listRowSeparatorLeading) { _ in
|
||||
-100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var accesibilityActions: some View {
|
||||
// Add the individual mentions as accessibility actions
|
||||
|
@ -139,17 +141,17 @@ public struct StatusRowView: View {
|
|||
routerPath.navigate(to: .accountDetail(id: mention.id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") {
|
||||
withAnimation {
|
||||
viewModel.displaySpoiler.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Button("@\(viewModel.status.account.username)") {
|
||||
routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id))
|
||||
}
|
||||
|
||||
|
||||
contextMenu
|
||||
}
|
||||
|
||||
|
@ -329,7 +331,7 @@ public struct StatusRowView: View {
|
|||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var contextMenuButton: some View {
|
||||
Menu {
|
||||
contextMenu
|
||||
|
@ -470,7 +472,7 @@ public struct StatusRowView: View {
|
|||
makeSwipeButton(action: preferences.swipeActionsStatusLeadingRight)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButton(action: StatusAction) -> some View {
|
||||
switch action {
|
||||
|
@ -500,14 +502,14 @@ public struct StatusRowView: View {
|
|||
await viewModel.unbookmark()
|
||||
} else {
|
||||
await
|
||||
viewModel.bookmark()
|
||||
viewModel.bookmark()
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButtonForRouterPath(action: StatusAction, destination: SheetDestinations) -> some View {
|
||||
Button {
|
||||
|
@ -520,9 +522,9 @@ public struct StatusRowView: View {
|
|||
}
|
||||
.tint(action.color(themeTintColor: theme.tintColor))
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private func makeSwipeButtonForTask(action: StatusAction, task: @escaping () async -> Void ) -> some View {
|
||||
private func makeSwipeButtonForTask(action: StatusAction, task: @escaping () async -> Void) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
|
|
|
@ -34,7 +34,7 @@ public class StatusRowViewModel: ObservableObject {
|
|||
@Published var rebloggers: [Account] = []
|
||||
|
||||
private let theme = Theme.shared
|
||||
|
||||
|
||||
var seen = false
|
||||
|
||||
var filter: Filtered? {
|
||||
|
@ -42,7 +42,6 @@ public class StatusRowViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
var highlightRowColor: Color {
|
||||
|
||||
if status.visibility == .direct {
|
||||
return theme.tintColor.opacity(0.15)
|
||||
} else if status.userMentioned != nil {
|
||||
|
@ -265,13 +264,13 @@ public class StatusRowViewModel: ObservableObject {
|
|||
_ = try await client.delete(endpoint: Statuses.status(id: status.id))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
func fetchActionsAccounts() async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
favoriters = try await client.get(endpoint: Statuses.favoritedBy(id: status.id, maxId: nil))
|
||||
rebloggers = try await client.get(endpoint: Statuses.rebloggedBy(id: status.id, maxId: nil))
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private func updateFromStatus(status: Status) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import SwiftUI
|
|||
|
||||
public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
|
||||
case local, federated, trending
|
||||
|
||||
|
||||
public func localizedTitle() -> LocalizedStringKey {
|
||||
switch self {
|
||||
case .federated:
|
||||
|
@ -16,7 +16,7 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
|
|||
return "timeline.trending"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func iconName() -> String {
|
||||
switch self {
|
||||
case .federated:
|
||||
|
@ -196,7 +196,7 @@ extension TimelineFilter: Codable {
|
|||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case .home:
|
||||
try container.encode(CodingKeys.home.rawValue, forKey: .home)
|
||||
|
@ -206,13 +206,13 @@ extension TimelineFilter: Codable {
|
|||
try container.encode(CodingKeys.federated.rawValue, forKey: .federated)
|
||||
case .trending:
|
||||
try container.encode(CodingKeys.trending.rawValue, forKey: .trending)
|
||||
case .hashtag(let tag, let accountId):
|
||||
case let .hashtag(tag, accountId):
|
||||
var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag)
|
||||
try nestedContainer.encode(tag)
|
||||
try nestedContainer.encode(accountId)
|
||||
case .list(let list):
|
||||
case let .list(list):
|
||||
try container.encode(list, forKey: .list)
|
||||
case .remoteLocal(let server, let filter):
|
||||
case let .remoteLocal(server, filter):
|
||||
var nestedContainer = container.nestedUnkeyedContainer(forKey: .hashtag)
|
||||
try nestedContainer.encode(server)
|
||||
try nestedContainer.encode(filter)
|
||||
|
@ -260,17 +260,17 @@ extension RemoteTimelineFilter: Codable {
|
|||
case .trending:
|
||||
self = .trending
|
||||
default:
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: container.codingPath,
|
||||
debugDescription: "Unabled to decode enum."
|
||||
)
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: container.codingPath,
|
||||
debugDescription: "Unabled to decode enum."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case .local:
|
||||
try container.encode(CodingKeys.local.rawValue, forKey: .local)
|
||||
|
|
|
@ -65,7 +65,8 @@ public struct TimelineView: View {
|
|||
if let collectionView,
|
||||
let index,
|
||||
let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0),
|
||||
rows > index {
|
||||
rows > index
|
||||
{
|
||||
collectionView.scrollToItem(at: .init(row: index, section: 0),
|
||||
at: .top,
|
||||
animated: viewModel.scrollToIndexAnimated)
|
||||
|
|
|
@ -34,7 +34,7 @@ class TimelineViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var timelineTask: Task<Void, Never>?
|
||||
|
||||
@Published var tag: Tag?
|
||||
|
@ -170,7 +170,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
// Hydrate statuses in the Timeline when statuses are empty.
|
||||
private func fetchFirstPage(client: Client) async throws {
|
||||
pendingStatusesObserver.pendingStatuses = []
|
||||
|
||||
|
||||
if statuses.isEmpty {
|
||||
statusesState = .loading
|
||||
}
|
||||
|
@ -208,7 +208,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
ReblogCache.shared.removeDuplicateReblogs(&statuses)
|
||||
|
||||
await cacheHome()
|
||||
|
||||
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||
}
|
||||
|
@ -238,7 +238,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
canStreamEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Return if task has been cancelled.
|
||||
guard !Task.isCancelled else {
|
||||
canStreamEvents = true
|
||||
|
@ -283,7 +283,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
canStreamEvents = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// We trigger a new fetch so we can get the next new statuses if any.
|
||||
// If none, it'll stop there.
|
||||
if !Task.isCancelled, let latest = statuses.first, let client {
|
||||
|
|
Loading…
Reference in a new issue