mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 10:11:00 +00:00
SwiftFormat
This commit is contained in:
parent
f1267620be
commit
6c307aba63
58 changed files with 313 additions and 299 deletions
|
@ -3,13 +3,13 @@ import AppAccount
|
||||||
import Conversations
|
import Conversations
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Explore
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import Lists
|
import Lists
|
||||||
import Models
|
import Models
|
||||||
import Status
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
import Explore
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
extension View {
|
extension View {
|
||||||
|
|
|
@ -112,7 +112,8 @@ struct IceCubesApp: App {
|
||||||
SideBarView(selectedTab: $selectedTab,
|
SideBarView(selectedTab: $selectedTab,
|
||||||
popToRootTab: $popToRootTab,
|
popToRootTab: $popToRootTab,
|
||||||
tabs: availableTabs,
|
tabs: availableTabs,
|
||||||
routerPath: sidebarRouterPath) {
|
routerPath: sidebarRouterPath)
|
||||||
|
{
|
||||||
GeometryReader { _ in
|
GeometryReader { _ in
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
@ -165,12 +166,12 @@ struct IceCubesApp: App {
|
||||||
popToRootTab = selectedTab
|
popToRootTab = selectedTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HapticManager.shared.fireHaptic(of: .tabSelection)
|
HapticManager.shared.fireHaptic(of: .tabSelection)
|
||||||
SoundEffectManager.shared.playSound(of: .tabSelection)
|
SoundEffectManager.shared.playSound(of: .tabSelection)
|
||||||
|
|
||||||
selectedTab = newTab
|
selectedTab = newTab
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if selectedTab == .notifications,
|
if selectedTab == .notifications,
|
||||||
let token = appAccountsManager.currentAccount.oauthToken
|
let token = appAccountsManager.currentAccount.oauthToken
|
||||||
|
@ -179,7 +180,7 @@ struct IceCubesApp: App {
|
||||||
watcher.unreadNotificationsCount = 0
|
watcher.unreadNotificationsCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})) {
|
})) {
|
||||||
ForEach(availableTabs) { tab in
|
ForEach(availableTabs) { tab in
|
||||||
tab.makeContentView(popToRootTab: $popToRootTab)
|
tab.makeContentView(popToRootTab: $popToRootTab)
|
||||||
|
|
|
@ -39,11 +39,11 @@ struct AboutView: View {
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
||||||
Label("settings.support.privacy-policy", systemImage: "lock")
|
Label("settings.support.privacy-policy", systemImage: "lock")
|
||||||
}
|
}
|
||||||
|
|
||||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) {
|
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) {
|
||||||
Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
|
Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ struct AboutView: View {
|
||||||
Text("\(versionNumber)©2023 Thomas Ricouard")
|
Text("\(versionNumber)©2023 Thomas Ricouard")
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Text("""
|
Text("""
|
||||||
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||||
|
|
|
@ -74,7 +74,7 @@ struct AccountSettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
if let token = appAccount.oauthToken {
|
if let token = appAccount.oauthToken {
|
||||||
|
|
|
@ -28,19 +28,17 @@ struct AddAccountView: View {
|
||||||
@State private var oauthURL: URL?
|
@State private var oauthURL: URL?
|
||||||
|
|
||||||
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
private var sanitizedName: String {
|
private var sanitizedName: String {
|
||||||
get {
|
var name = instanceName
|
||||||
var name = instanceName
|
.replacingOccurrences(of: "http://", with: "")
|
||||||
.replacingOccurrences(of: "http://", with: "")
|
.replacingOccurrences(of: "https://", with: "")
|
||||||
.replacingOccurrences(of: "https://", with: "")
|
|
||||||
|
if name.contains("@") {
|
||||||
if name.contains("@") {
|
let parts = name.components(separatedBy: "@")
|
||||||
let parts = name.components(separatedBy: "@")
|
name = parts[parts.count - 1] // [@]username@server.address.com
|
||||||
name = parts[parts.count-1] // [@]username@server.address.com
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
}
|
||||||
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
@FocusState private var isInstanceURLFieldFocused: Bool
|
@FocusState private var isInstanceURLFieldFocused: Bool
|
||||||
|
@ -94,8 +92,8 @@ struct AddAccountView: View {
|
||||||
.onChange(of: instanceName) { newValue in
|
.onChange(of: instanceName) { newValue in
|
||||||
instanceNamePublisher.send(newValue)
|
instanceNamePublisher.send(newValue)
|
||||||
}
|
}
|
||||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { _ in
|
||||||
//let newValue = newValue
|
// let newValue = newValue
|
||||||
// .replacingOccurrences(of: "http://", with: "")
|
// .replacingOccurrences(of: "http://", with: "")
|
||||||
// .replacingOccurrences(of: "https://", with: "")
|
// .replacingOccurrences(of: "https://", with: "")
|
||||||
let client = Client(server: sanitizedName)
|
let client = Client(server: sanitizedName)
|
||||||
|
@ -106,7 +104,7 @@ struct AddAccountView: View {
|
||||||
let instance: Instance = try await client.get(endpoint: Instances.instance)
|
let instance: Instance = try await client.get(endpoint: Instances.instance)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
|
self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
|
||||||
}
|
}
|
||||||
instanceFetchError = nil
|
instanceFetchError = nil
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -48,14 +48,14 @@ struct ContentSettingsView: View {
|
||||||
Text("settings.content.expand-spoilers")
|
Text("settings.content.expand-spoilers")
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
|
|
||||||
Picker("settings.content.expand-media", selection: $userPreferences.appAutoExpandMedia) {
|
Picker("settings.content.expand-media", selection: $userPreferences.appAutoExpandMedia) {
|
||||||
ForEach(ServerPreferences.AutoExpandMedia.allCases, id: \.rawValue) { media in
|
ForEach(ServerPreferences.AutoExpandMedia.allCases, id: \.rawValue) { media in
|
||||||
Text(media.description).tag(media)
|
Text(media.description).tag(media)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(userPreferences.useInstanceContentSettings)
|
.disabled(userPreferences.useInstanceContentSettings)
|
||||||
|
|
||||||
Toggle(isOn: $userPreferences.collapseLongPosts) {
|
Toggle(isOn: $userPreferences.collapseLongPosts) {
|
||||||
Text("settings.content.collapse-long-posts")
|
Text("settings.content.collapse-long-posts")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Status
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
|
||||||
|
|
||||||
class DisplaySettingsLocalColors: ObservableObject {
|
class DisplaySettingsLocalColors: ObservableObject {
|
||||||
@Published var tintColor = Theme.shared.tintColor
|
@Published var tintColor = Theme.shared.tintColor
|
||||||
@Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
@Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
|
||||||
@Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
|
@Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
|
||||||
@Published var labelColor = Theme.shared.labelColor
|
@Published var labelColor = Theme.shared.labelColor
|
||||||
|
|
||||||
private var subscriptions = Set<AnyCancellable>()
|
private var subscriptions = Set<AnyCancellable>()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
$tintColor
|
$tintColor
|
||||||
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
||||||
.sink(receiveValue: { newColor in Theme.shared.tintColor = newColor } )
|
.sink(receiveValue: { newColor in Theme.shared.tintColor = newColor })
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
$primaryBackgroundColor
|
$primaryBackgroundColor
|
||||||
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
||||||
.sink(receiveValue: { newColor in Theme.shared.primaryBackgroundColor = newColor } )
|
.sink(receiveValue: { newColor in Theme.shared.primaryBackgroundColor = newColor })
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
$secondaryBackgroundColor
|
$secondaryBackgroundColor
|
||||||
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
||||||
.sink(receiveValue: { newColor in Theme.shared.secondaryBackgroundColor = newColor } )
|
.sink(receiveValue: { newColor in Theme.shared.secondaryBackgroundColor = newColor })
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
$labelColor
|
$labelColor
|
||||||
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
||||||
.sink(receiveValue: { newColor in Theme.shared.labelColor = newColor } )
|
.sink(receiveValue: { newColor in Theme.shared.labelColor = newColor })
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,15 +40,15 @@ struct DisplaySettingsView: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var userPreferences: UserPreferences
|
@EnvironmentObject private var userPreferences: UserPreferences
|
||||||
|
|
||||||
@StateObject private var localColors = DisplaySettingsLocalColors()
|
@StateObject private var localColors = DisplaySettingsLocalColors()
|
||||||
|
|
||||||
@State private var isFontSelectorPresented = false
|
@State private var isFontSelectorPresented = false
|
||||||
|
|
||||||
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
|
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
|
||||||
client: Client(server: ""),
|
client: Client(server: ""),
|
||||||
routerPath: RouterPath()) // translate from latin button
|
routerPath: RouterPath()) // translate from latin button
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
exampleSection
|
exampleSection
|
||||||
|
@ -62,14 +62,14 @@ struct DisplaySettingsView: View {
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var exampleSection: some View {
|
private var exampleSection: some View {
|
||||||
Section("settings.display.example-toot") {
|
Section("settings.display.example-toot") {
|
||||||
StatusRowView(viewModel: { previewStatusViewModel })
|
StatusRowView(viewModel: { previewStatusViewModel })
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var themeSection: some View {
|
private var themeSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme)
|
Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme)
|
||||||
|
@ -97,7 +97,7 @@ struct DisplaySettingsView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fontSection: some View {
|
private var fontSection: some View {
|
||||||
Section("settings.display.section.font") {
|
Section("settings.display.section.font") {
|
||||||
Picker("settings.display.font", selection: .init(get: { () -> FontState in
|
Picker("settings.display.font", selection: .init(get: { () -> FontState in
|
||||||
|
@ -140,7 +140,7 @@ struct DisplaySettingsView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var layoutSection: some View {
|
private var layoutSection: some View {
|
||||||
Section("settings.display.section.display") {
|
Section("settings.display.section.display") {
|
||||||
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
|
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
|
||||||
|
@ -169,7 +169,7 @@ struct DisplaySettingsView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var platformsSection: some View {
|
private var platformsSection: some View {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
@ -186,7 +186,7 @@ struct DisplaySettingsView: View {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var resetSection: some View {
|
private var resetSection: some View {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
|
@ -195,12 +195,12 @@ struct DisplaySettingsView: View {
|
||||||
theme.avatarShape = .rounded
|
theme.avatarShape = .rounded
|
||||||
theme.avatarPosition = .top
|
theme.avatarPosition = .top
|
||||||
theme.statusActionsDisplay = .full
|
theme.statusActionsDisplay = .full
|
||||||
|
|
||||||
localColors.tintColor = theme.tintColor
|
localColors.tintColor = theme.tintColor
|
||||||
localColors.primaryBackgroundColor = theme.primaryBackgroundColor
|
localColors.primaryBackgroundColor = theme.primaryBackgroundColor
|
||||||
localColors.secondaryBackgroundColor = theme.secondaryBackgroundColor
|
localColors.secondaryBackgroundColor = theme.secondaryBackgroundColor
|
||||||
localColors.labelColor = theme.labelColor
|
localColors.labelColor = theme.labelColor
|
||||||
|
|
||||||
} label: {
|
} label: {
|
||||||
Text("settings.display.restore")
|
Text("settings.display.restore")
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
|
||||||
@State private var loadingProducts: Bool = false
|
@State private var loadingProducts: Bool = false
|
||||||
|
@ -86,7 +86,7 @@ struct SupportAppView: View {
|
||||||
refreshUserInfo()
|
refreshUserInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func purchase(product: StoreProduct) async {
|
private func purchase(product: StoreProduct) async {
|
||||||
if !isProcessingPurchase {
|
if !isProcessingPurchase {
|
||||||
isProcessingPurchase = true
|
isProcessingPurchase = true
|
||||||
|
@ -101,23 +101,23 @@ struct SupportAppView: View {
|
||||||
isProcessingPurchase = false
|
isProcessingPurchase = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchStoreProducts() {
|
private func fetchStoreProducts() {
|
||||||
Purchases.shared.getProducts(Tip.allCases.map { $0.productId }) { products in
|
Purchases.shared.getProducts(Tip.allCases.map { $0.productId }) { products in
|
||||||
self.subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId })
|
self.subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId })
|
||||||
self.products = products.filter{ $0.productIdentifier != Tip.supporter.productId}.sorted(by: { $0.price < $1.price })
|
self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted(by: { $0.price < $1.price })
|
||||||
withAnimation {
|
withAnimation {
|
||||||
loadingProducts = false
|
loadingProducts = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshUserInfo() {
|
private func refreshUserInfo() {
|
||||||
Purchases.shared.getCustomerInfo { info, _ in
|
Purchases.shared.getCustomerInfo { info, _ in
|
||||||
self.customerInfo = info
|
self.customerInfo = info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makePurchaseButton(product: StoreProduct) -> some View {
|
private func makePurchaseButton(product: StoreProduct) -> some View {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
|
@ -133,7 +133,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var aboutSection: some View {
|
private var aboutSection: some View {
|
||||||
Section {
|
Section {
|
||||||
HStack(alignment: .top, spacing: 12) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
|
@ -152,7 +152,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var subscriptionSection: some View {
|
private var subscriptionSection: some View {
|
||||||
Section {
|
Section {
|
||||||
if loadingProducts {
|
if loadingProducts {
|
||||||
|
@ -163,14 +163,14 @@ struct SupportAppView: View {
|
||||||
Text(Image(systemName: "checkmark.seal.fill"))
|
Text(Image(systemName: "checkmark.seal.fill"))
|
||||||
.foregroundColor(theme.tintColor)
|
.foregroundColor(theme.tintColor)
|
||||||
.baselineOffset(-1) +
|
.baselineOffset(-1) +
|
||||||
Text("settings.support.supporter.subscribed")
|
Text("settings.support.supporter.subscribed")
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
} else {
|
} else {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(Image(systemName: "checkmark.seal.fill"))
|
Text(Image(systemName: "checkmark.seal.fill"))
|
||||||
.foregroundColor(theme.tintColor)
|
.foregroundColor(theme.tintColor)
|
||||||
.baselineOffset(-1) +
|
.baselineOffset(-1) +
|
||||||
Text(Tip.supporter.title)
|
Text(Tip.supporter.title)
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
Text(Tip.supporter.subtitle)
|
Text(Tip.supporter.subtitle)
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
|
@ -179,7 +179,6 @@ struct SupportAppView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
makePurchaseButton(product: subscription)
|
makePurchaseButton(product: subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
@ -190,7 +189,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tipsSection: some View {
|
private var tipsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
if loadingProducts {
|
if loadingProducts {
|
||||||
|
@ -215,7 +214,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var restorePurchase: some View {
|
private var restorePurchase: some View {
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -234,7 +233,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var linksSection: some View {
|
private var linksSection: some View {
|
||||||
Section {
|
Section {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
@ -254,7 +253,7 @@ struct SupportAppView: View {
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.secondaryBackgroundColor)
|
.listRowBackground(theme.secondaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingPlaceholder: some View {
|
private var loadingPlaceholder: some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Account
|
import Account
|
||||||
|
import DesignSystem
|
||||||
import Explore
|
import Explore
|
||||||
import Foundation
|
import Foundation
|
||||||
import Status
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import DesignSystem
|
|
||||||
|
|
||||||
enum Tab: Int, Identifiable, Hashable {
|
enum Tab: Int, Identifiable, Hashable {
|
||||||
case timeline, notifications, mentions, explore, messages, settings, other
|
case timeline, notifications, mentions, explore, messages, settings, other
|
||||||
|
|
|
@ -54,7 +54,8 @@ class ShareViewController: UIViewController {
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose,
|
NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose,
|
||||||
object: nil,
|
object: nil,
|
||||||
queue: nil) { _ in
|
queue: nil)
|
||||||
|
{ _ in
|
||||||
self.close()
|
self.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
import Env
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct AccountDetailContextMenu: View {
|
public struct AccountDetailContextMenu: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
|
|
||||||
@ObservedObject var viewModel: AccountDetailViewModel
|
@ObservedObject var viewModel: AccountDetailViewModel
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if let account = viewModel.account {
|
if let account = viewModel.account {
|
||||||
Section(account.acct) {
|
Section(account.acct) {
|
||||||
|
|
|
@ -51,7 +51,8 @@ public struct AccountDetailView: View {
|
||||||
|
|
||||||
Picker("", selection: $viewModel.selectedTab) {
|
Picker("", selection: $viewModel.selectedTab) {
|
||||||
ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs,
|
ForEach(isCurrentUser ? AccountDetailViewModel.Tab.currentAccountTabs : AccountDetailViewModel.Tab.accountTabs,
|
||||||
id: \.self) { tab in
|
id: \.self)
|
||||||
|
{ tab in
|
||||||
Image(systemName: tab.iconName)
|
Image(systemName: tab.iconName)
|
||||||
.tag(tab)
|
.tag(tab)
|
||||||
}
|
}
|
||||||
|
@ -300,7 +301,7 @@ public struct AccountDetailView: View {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Menu {
|
Menu {
|
||||||
AccountDetailContextMenu(viewModel: viewModel)
|
AccountDetailContextMenu(viewModel: viewModel)
|
||||||
|
|
||||||
if !viewModel.isCurrentUser {
|
if !viewModel.isCurrentUser {
|
||||||
Button {
|
Button {
|
||||||
isEditingRelationshipNote = true
|
isEditingRelationshipNote = true
|
||||||
|
@ -308,7 +309,7 @@ public struct AccountDetailView: View {
|
||||||
Label("account.relation.note.edit", systemImage: "pencil")
|
Label("account.relation.note.edit", systemImage: "pencil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCurrentUser {
|
if isCurrentUser {
|
||||||
Button {
|
Button {
|
||||||
isEditingAccount = true
|
isEditingAccount = true
|
||||||
|
@ -329,10 +330,10 @@ public struct AccountDetailView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Label("settings.push.navigation-title", systemImage: "bell")
|
Label("settings.push.navigation-title", systemImage: "bell")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let account = viewModel.account {
|
if let account = viewModel.account {
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") {
|
if let url = URL(string: "https://mastometrics.com/auth/login?username=\(account.acct)@\(client.server)&instance=\(client.server)&auto=true") {
|
||||||
openURL(url)
|
openURL(url)
|
||||||
|
@ -343,7 +344,7 @@ public struct AccountDetailView: View {
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
routerPath.presentedSheet = .settings
|
routerPath.presentedSheet = .settings
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -137,9 +137,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
featuredTags: featuredTags,
|
featuredTags: featuredTags,
|
||||||
relationships: relationships)
|
relationships: relationships)
|
||||||
} catch {
|
} catch {
|
||||||
return try await .init(account: account,
|
return try await .init(account: account,
|
||||||
featuredTags: [],
|
featuredTags: [],
|
||||||
relationships: relationships)
|
relationships: relationships)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return try await .init(account: account,
|
return try await .init(account: account,
|
||||||
|
|
|
@ -26,9 +26,9 @@ public struct AccountsListRow: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
@StateObject var viewModel: AccountsListRowViewModel
|
@StateObject var viewModel: AccountsListRowViewModel
|
||||||
|
|
||||||
@State private var isEditingRelationshipNote: Bool = false
|
@State private var isEditingRelationshipNote: Bool = false
|
||||||
|
|
||||||
let isFollowRequest: Bool
|
let isFollowRequest: Bool
|
||||||
let requestUpdated: (() -> Void)?
|
let requestUpdated: (() -> Void)?
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ public struct AccountsListRow: View {
|
||||||
AccountDetailHeaderView(viewModel: .init(account: viewModel.account),
|
AccountDetailHeaderView(viewModel: .init(account: viewModel.account),
|
||||||
account: viewModel.account,
|
account: viewModel.account,
|
||||||
scrollViewProxy: nil)
|
scrollViewProxy: nil)
|
||||||
.applyAccountDetailsRowStyle(theme: theme)
|
.applyAccountDetailsRowStyle(theme: theme)
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
@ -98,6 +98,5 @@ public struct AccountsListRow: View {
|
||||||
.environmentObject(currentAccount)
|
.environmentObject(currentAccount)
|
||||||
.environmentObject(client)
|
.environmentObject(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,9 @@ struct EditFilterView: View {
|
||||||
@FocusState private var isTitleFocused: Bool
|
@FocusState private var isTitleFocused: Bool
|
||||||
|
|
||||||
private var data: ServerFilterData {
|
private var data: ServerFilterData {
|
||||||
var expiresIn: String? = nil;
|
var expiresIn: String?
|
||||||
// we add 50 seconds, otherwise we immediately show 6d for a 7d filter (6d, 23h, 59s)
|
// we add 50 seconds, otherwise we immediately show 6d for a 7d filter (6d, 23h, 59s)
|
||||||
switch(expirySelection){
|
switch expirySelection {
|
||||||
case .infinite:
|
case .infinite:
|
||||||
expiresIn = "" // need to send an empty value in order for the server to clear this field in the filter
|
expiresIn = "" // need to send an empty value in order for the server to clear this field in the filter
|
||||||
case .custom:
|
case .custom:
|
||||||
|
@ -33,11 +33,11 @@ struct EditFilterView: View {
|
||||||
default:
|
default:
|
||||||
expiresIn = String(expirySelection.rawValue + 50)
|
expiresIn = String(expirySelection.rawValue + 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ServerFilterData(title: title,
|
return ServerFilterData(title: title,
|
||||||
context: contexts,
|
context: contexts,
|
||||||
filterAction: filterAction,
|
filterAction: filterAction,
|
||||||
expiresIn: expiresIn)
|
expiresIn: expiresIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var canSave: Bool {
|
private var canSave: Bool {
|
||||||
|
@ -53,7 +53,7 @@ struct EditFilterView: View {
|
||||||
_expiresAt = .init(initialValue: filter?.expiresAt?.asDate)
|
_expiresAt = .init(initialValue: filter?.expiresAt?.asDate)
|
||||||
_expirySelection = .init(initialValue: filter?.expiresAt == nil ? .infinite : .custom)
|
_expirySelection = .init(initialValue: filter?.expiresAt == nil ? .infinite : .custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
titleSection
|
titleSection
|
||||||
|
@ -95,15 +95,14 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
if expirySelection != .infinite {
|
if expirySelection != .infinite {
|
||||||
DatePicker("filter.edit.expiry.date-time",
|
DatePicker("filter.edit.expiry.date-time",
|
||||||
selection: Binding<Date>(get: {self.expiresAt ?? Date()}, set: {self.expiresAt = $0}),
|
selection: Binding<Date>(get: { self.expiresAt ?? Date() }, set: { self.expiresAt = $0 }),
|
||||||
displayedComponents: [.date, .hourAndMinute]
|
displayedComponents: [.date, .hourAndMinute])
|
||||||
)
|
.disabled(expirySelection != .custom)
|
||||||
.disabled(expirySelection != .custom)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var titleSection: some View {
|
private var titleSection: some View {
|
||||||
Section("filter.edit.title") {
|
Section("filter.edit.title") {
|
||||||
|
@ -116,7 +115,7 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
if filter == nil, !title.isEmpty {
|
if filter == nil, !title.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -7,7 +7,7 @@ public struct AppAccountView: View {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
|
|
||||||
@StateObject var viewModel: AppAccountViewModel
|
@StateObject var viewModel: AppAccountViewModel
|
||||||
|
|
||||||
public init(viewModel: AppAccountViewModel) {
|
public init(viewModel: AppAccountViewModel) {
|
||||||
|
@ -51,7 +51,8 @@ public struct AppAccountView: View {
|
||||||
.offset(x: 5, y: -5)
|
.offset(x: 5, y: -5)
|
||||||
} else if viewModel.showBadge,
|
} else if viewModel.showBadge,
|
||||||
let token = viewModel.appAccount.oauthToken,
|
let token = viewModel.appAccount.oauthToken,
|
||||||
preferences.getNotificationsCount(for: token) > 0 {
|
preferences.getNotificationsCount(for: token) > 0
|
||||||
|
{
|
||||||
let notificationsCount = preferences.getNotificationsCount(for: token)
|
let notificationsCount = preferences.getNotificationsCount(for: token)
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
|
|
|
@ -23,7 +23,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
.map { preferences.getNotificationsCount(for: $0) }
|
.map { preferences.getNotificationsCount(for: $0) }
|
||||||
.reduce(0, +) > 0
|
.reduce(0, +) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var preferredHeight: CGFloat {
|
private var preferredHeight: CGFloat {
|
||||||
var baseHeight: CGFloat = 220
|
var baseHeight: CGFloat = 220
|
||||||
baseHeight += CGFloat(60 * accountsViewModel.count)
|
baseHeight += CGFloat(60 * accountsViewModel.count)
|
||||||
|
@ -48,9 +48,9 @@ public struct AppAccountsSelectorView: View {
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isPresented, content: {
|
.sheet(isPresented: $isPresented, content: {
|
||||||
accountsView.presentationDetents([.height(preferredHeight), .large])
|
accountsView.presentationDetents([.height(preferredHeight), .large])
|
||||||
.onAppear {
|
.onAppear {
|
||||||
refreshAccounts()
|
refreshAccounts()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onChange(of: currentAccount.account?.id) { _ in
|
.onChange(of: currentAccount.account?.id) { _ in
|
||||||
refreshAccounts()
|
refreshAccounts()
|
||||||
|
@ -88,7 +88,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
|
||||||
if accountCreationEnabled {
|
if accountCreationEnabled {
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
|
@ -121,7 +121,7 @@ public struct AppAccountsSelectorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var settingsButton: some View {
|
private var settingsButton: some View {
|
||||||
Button {
|
Button {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
|
|
|
@ -177,7 +177,8 @@ struct ConversationMessageView: View {
|
||||||
let width = mediaWidth(proxy: proxy)
|
let width = mediaWidth(proxy: proxy)
|
||||||
if let url = attachement.url {
|
if let url = attachement.url {
|
||||||
LazyImage(request: makeImageRequest(for: url,
|
LazyImage(request: makeImageRequest(for: url,
|
||||||
size: .init(width: width, height: 200))) { state in
|
size: .init(width: width, height: 200)))
|
||||||
|
{ state in
|
||||||
if let image = state.image {
|
if let image = state.image {
|
||||||
image
|
image
|
||||||
.resizable()
|
.resizable()
|
||||||
|
|
|
@ -47,7 +47,8 @@ public struct ConversationsListView: View {
|
||||||
} else if viewModel.isError {
|
} else if viewModel.isError {
|
||||||
ErrorView(title: "conversations.error.title",
|
ErrorView(title: "conversations.error.title",
|
||||||
message: "conversations.error.message",
|
message: "conversations.error.message",
|
||||||
buttonTitle: "conversations.error.button") {
|
buttonTitle: "conversations.error.button")
|
||||||
|
{
|
||||||
Task {
|
Task {
|
||||||
await viewModel.fetchConversations()
|
await viewModel.fetchConversations()
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,15 +44,15 @@ public extension Font {
|
||||||
static var scaledHeadline: Font {
|
static var scaledHeadline: Font {
|
||||||
customFont(size: userScaledFontSize(baseSize: headline), relativeTo: .headline).weight(.semibold)
|
customFont(size: userScaledFontSize(baseSize: headline), relativeTo: .headline).weight(.semibold)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var scaledHeadlineFont: UIFont {
|
static var scaledHeadlineFont: UIFont {
|
||||||
customUIFont(size: userScaledFontSize(baseSize: headline))
|
customUIFont(size: userScaledFontSize(baseSize: headline))
|
||||||
}
|
}
|
||||||
|
|
||||||
static var scaledBodyFocused: Font {
|
static var scaledBodyFocused: Font {
|
||||||
customFont(size: userScaledFontSize(baseSize: body + 2), relativeTo: .body)
|
customFont(size: userScaledFontSize(baseSize: body + 2), relativeTo: .body)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var scaledBodyFocusedFont: UIFont {
|
static var scaledBodyFocusedFont: UIFont {
|
||||||
customUIFont(size: userScaledFontSize(baseSize: body + 2))
|
customUIFont(size: userScaledFontSize(baseSize: body + 2))
|
||||||
}
|
}
|
||||||
|
@ -109,11 +109,13 @@ public extension UIFont {
|
||||||
}
|
}
|
||||||
return UIFont(descriptor: descriptor, size: pointSize)
|
return UIFont(descriptor: descriptor, size: pointSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
var emojiSize: CGFloat {
|
var emojiSize: CGFloat {
|
||||||
self.pointSize
|
pointSize
|
||||||
}
|
}
|
||||||
|
|
||||||
var emojiBaselineOffset: CGFloat {
|
var emojiBaselineOffset: CGFloat {
|
||||||
// Center emoji with capital letter size of font
|
// Center emoji with capital letter size of font
|
||||||
-(self.emojiSize - self.capHeight) / 2
|
-(emojiSize - capHeight) / 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,12 @@ import SwiftUI
|
||||||
// images named in lower case are Apple's symbols
|
// images named in lower case are Apple's symbols
|
||||||
// images inamed in CamelCase are custom
|
// images inamed in CamelCase are custom
|
||||||
|
|
||||||
extension Label where Title == Text, Icon == Image {
|
public extension Label where Title == Text, Icon == Image {
|
||||||
|
init(_ title: LocalizedStringKey, imageNamed: String) {
|
||||||
public init (_ title: LocalizedStringKey, imageNamed: String) {
|
|
||||||
if imageNamed.lowercased() == imageNamed {
|
if imageNamed.lowercased() == imageNamed {
|
||||||
self.init(title, systemImage: imageNamed)
|
self.init(title, systemImage: imageNamed)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
self.init(title, image: imageNamed)
|
self.init(title, image: imageNamed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ public extension EnvironmentValues {
|
||||||
get { self[IsInCaptureMode.self] }
|
get { self[IsInCaptureMode.self] }
|
||||||
set { self[IsInCaptureMode.self] = newValue }
|
set { self[IsInCaptureMode.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSupporter: Bool {
|
var isSupporter: Bool {
|
||||||
get { self[IsSupporter.self] }
|
get { self[IsSupporter.self] }
|
||||||
set { self[IsSupporter.self] = newValue }
|
set { self[IsSupporter.self] = newValue }
|
||||||
|
|
|
@ -36,7 +36,7 @@ public enum Duration: Int, CaseIterable {
|
||||||
return "enum.durations.custom"
|
return "enum.durations.custom"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func mutingDurations() -> [Duration] {
|
public static func mutingDurations() -> [Duration] {
|
||||||
return Self.allCases.filter { $0 != .custom }
|
return Self.allCases.filter { $0 != .custom }
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ public enum Duration: Int, CaseIterable {
|
||||||
public static func filterDurations() -> [Duration] {
|
public static func filterDurations() -> [Duration] {
|
||||||
return [.infinite, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .sevenDays, .custom]
|
return [.infinite, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .sevenDays, .custom]
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func pollDurations() -> [Duration] {
|
public static func pollDurations() -> [Duration] {
|
||||||
return [.fiveMinutes, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .threeDays, .sevenDays]
|
return [.fiveMinutes, .thirtyMinutes, .oneHour, .sixHours, .twelveHours, .oneDay, .threeDays, .sevenDays]
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,6 @@ public class RouterPath: ObservableObject {
|
||||||
@Published public var path: [RouterDestination] = []
|
@Published public var path: [RouterDestination] = []
|
||||||
@Published public var presentedSheet: SheetDestination?
|
@Published public var presentedSheet: SheetDestination?
|
||||||
|
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public func navigate(to: RouterDestination) {
|
public func navigate(to: RouterDestination) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import AVKit
|
||||||
import CoreHaptics
|
import CoreHaptics
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVKit
|
|
||||||
|
|
||||||
public class SoundEffectManager {
|
public class SoundEffectManager {
|
||||||
public static let shared: SoundEffectManager = .init()
|
public static let shared: SoundEffectManager = .init()
|
||||||
|
@ -13,7 +13,7 @@ public class SoundEffectManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private let userPreferences = UserPreferences.shared
|
private let userPreferences = UserPreferences.shared
|
||||||
|
|
||||||
private var currentPlayer: AVAudioPlayer?
|
private var currentPlayer: AVAudioPlayer?
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public protocol StatusDataControlling: ObservableObject {
|
public protocol StatusDataControlling: ObservableObject {
|
||||||
var isReblogged: Bool { get set }
|
var isReblogged: Bool { get set }
|
||||||
var isBookmarked: Bool { get set }
|
var isBookmarked: Bool { get set }
|
||||||
var isFavorited: Bool { get set }
|
var isFavorited: Bool { get set }
|
||||||
|
|
||||||
var favoritesCount: Int { get set }
|
var favoritesCount: Int { get set }
|
||||||
var reblogsCount: Int { get set }
|
var reblogsCount: Int { get set }
|
||||||
var repliesCount: Int { get set }
|
var repliesCount: Int { get set }
|
||||||
|
|
||||||
func toggleBookmark(remoteStatus: String?) async
|
func toggleBookmark(remoteStatus: String?) async
|
||||||
func toggleReblog(remoteStatus: String?) async
|
func toggleReblog(remoteStatus: String?) async
|
||||||
func toggleFavorite(remoteStatus: String?) async
|
func toggleFavorite(remoteStatus: String?) async
|
||||||
|
@ -21,14 +21,14 @@ public protocol StatusDataControlling: ObservableObject {
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class StatusDataControllerProvider {
|
public final class StatusDataControllerProvider {
|
||||||
public static let shared = StatusDataControllerProvider()
|
public static let shared = StatusDataControllerProvider()
|
||||||
|
|
||||||
private var cache: NSMutableDictionary = [:]
|
private var cache: NSMutableDictionary = [:]
|
||||||
|
|
||||||
private struct CacheKey: Hashable {
|
private struct CacheKey: Hashable {
|
||||||
let statusId: String
|
let statusId: String
|
||||||
let client: Client
|
let client: Client
|
||||||
}
|
}
|
||||||
|
|
||||||
public func dataController(for status: any AnyStatus, client: Client) -> StatusDataController {
|
public func dataController(for status: any AnyStatus, client: Client) -> StatusDataController {
|
||||||
let key = CacheKey(statusId: status.id, client: client)
|
let key = CacheKey(statusId: status.id, client: client)
|
||||||
if let controller = cache[key] as? StatusDataController {
|
if let controller = cache[key] as? StatusDataController {
|
||||||
|
@ -38,7 +38,7 @@ public final class StatusDataControllerProvider {
|
||||||
cache[key] = controller
|
cache[key] = controller
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateDataControllers(for statuses: [Status], client: Client) {
|
public func updateDataControllers(for statuses: [Status], client: Client) {
|
||||||
for status in statuses {
|
for status in statuses {
|
||||||
let realStatus: AnyStatus = status.reblog ?? status
|
let realStatus: AnyStatus = status.reblog ?? status
|
||||||
|
@ -52,42 +52,42 @@ public final class StatusDataControllerProvider {
|
||||||
public final class StatusDataController: StatusDataControlling {
|
public final class StatusDataController: StatusDataControlling {
|
||||||
private let status: AnyStatus
|
private let status: AnyStatus
|
||||||
private let client: Client
|
private let client: Client
|
||||||
|
|
||||||
public var isReblogged: Bool
|
public var isReblogged: Bool
|
||||||
public var isBookmarked: Bool
|
public var isBookmarked: Bool
|
||||||
public var isFavorited: Bool
|
public var isFavorited: Bool
|
||||||
|
|
||||||
public var favoritesCount: Int
|
public var favoritesCount: Int
|
||||||
public var reblogsCount: Int
|
public var reblogsCount: Int
|
||||||
public var repliesCount: Int
|
public var repliesCount: Int
|
||||||
|
|
||||||
init(status: AnyStatus, client: Client) {
|
init(status: AnyStatus, client: Client) {
|
||||||
self.status = status
|
self.status = status
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
self.isReblogged = status.reblogged == true
|
isReblogged = status.reblogged == true
|
||||||
self.isBookmarked = status.bookmarked == true
|
isBookmarked = status.bookmarked == true
|
||||||
self.isFavorited = status.favourited == true
|
isFavorited = status.favourited == true
|
||||||
|
|
||||||
self.reblogsCount = status.reblogsCount
|
reblogsCount = status.reblogsCount
|
||||||
self.repliesCount = status.repliesCount
|
repliesCount = status.repliesCount
|
||||||
self.favoritesCount = status.favouritesCount
|
favoritesCount = status.favouritesCount
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateFrom(status: AnyStatus, publishUpdate: Bool) {
|
public func updateFrom(status: AnyStatus, publishUpdate: Bool) {
|
||||||
self.isReblogged = status.reblogged == true
|
isReblogged = status.reblogged == true
|
||||||
self.isBookmarked = status.bookmarked == true
|
isBookmarked = status.bookmarked == true
|
||||||
self.isFavorited = status.favourited == true
|
isFavorited = status.favourited == true
|
||||||
|
|
||||||
self.reblogsCount = status.reblogsCount
|
reblogsCount = status.reblogsCount
|
||||||
self.repliesCount = status.repliesCount
|
repliesCount = status.repliesCount
|
||||||
self.favoritesCount = status.favouritesCount
|
favoritesCount = status.favouritesCount
|
||||||
|
|
||||||
if publishUpdate {
|
if publishUpdate {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func toggleFavorite(remoteStatus: String?) async {
|
public func toggleFavorite(remoteStatus: String?) async {
|
||||||
guard client.isAuth else { return }
|
guard client.isAuth else { return }
|
||||||
isFavorited.toggle()
|
isFavorited.toggle()
|
||||||
|
@ -104,8 +104,7 @@ public final class StatusDataController: StatusDataControlling {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func toggleReblog(remoteStatus: String?) async {
|
public func toggleReblog(remoteStatus: String?) async {
|
||||||
guard client.isAuth else { return }
|
guard client.isAuth else { return }
|
||||||
isReblogged.toggle()
|
isReblogged.toggle()
|
||||||
|
@ -122,7 +121,7 @@ public final class StatusDataController: StatusDataControlling {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func toggleBookmark(remoteStatus: String?) async {
|
public func toggleBookmark(remoteStatus: String?) async {
|
||||||
guard client.isAuth else { return }
|
guard client.isAuth else { return }
|
||||||
isBookmarked.toggle()
|
isBookmarked.toggle()
|
||||||
|
|
|
@ -7,7 +7,7 @@ public class StatusEmbedCache {
|
||||||
public static let shared = StatusEmbedCache()
|
public static let shared = StatusEmbedCache()
|
||||||
|
|
||||||
private var cache: [URL: Status] = [:]
|
private var cache: [URL: Status] = [:]
|
||||||
|
|
||||||
public var badStatusesURLs = Set<URL>()
|
public var badStatusesURLs = Set<URL>()
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
|
@ -49,7 +49,7 @@ public class UserPreferences: ObservableObject {
|
||||||
@AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText
|
@AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText
|
||||||
|
|
||||||
@AppStorage("requested_review") public var requestedReview = false
|
@AppStorage("requested_review") public var requestedReview = false
|
||||||
|
|
||||||
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
|
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
|
||||||
|
|
||||||
public enum SwipeActionsIconStyle: String, CaseIterable {
|
public enum SwipeActionsIconStyle: String, CaseIterable {
|
||||||
|
@ -70,7 +70,7 @@ public class UserPreferences: ObservableObject {
|
||||||
// Main actor-isolated static property 'allCases' cannot be used to
|
// Main actor-isolated static property 'allCases' cannot be used to
|
||||||
// satisfy nonisolated protocol requirement
|
// satisfy nonisolated protocol requirement
|
||||||
//
|
//
|
||||||
nonisolated public static var allCases: [Self] {
|
public nonisolated static var allCases: [Self] {
|
||||||
[.iconWithText, .iconOnly]
|
[.iconWithText, .iconOnly]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,12 +127,13 @@ public struct ExploreView: View {
|
||||||
private var suggestedAccountsSection: some View {
|
private var suggestedAccountsSection: some View {
|
||||||
Section("explore.section.suggested-users") {
|
Section("explore.section.suggested-users") {
|
||||||
ForEach(viewModel.suggestedAccounts
|
ForEach(viewModel.suggestedAccounts
|
||||||
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
|
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count))
|
||||||
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
{ account in
|
||||||
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
AccountsListRow(viewModel: .init(account: account, relationShip: relationship))
|
||||||
}
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) {
|
NavigationLink(value: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts)) {
|
||||||
Text("see-more")
|
Text("see-more")
|
||||||
.foregroundColor(theme.tintColor)
|
.foregroundColor(theme.tintColor)
|
||||||
|
@ -144,11 +145,12 @@ public struct ExploreView: View {
|
||||||
private var trendingTagsSection: some View {
|
private var trendingTagsSection: some View {
|
||||||
Section("explore.section.trending.tags") {
|
Section("explore.section.trending.tags") {
|
||||||
ForEach(viewModel.trendingTags
|
ForEach(viewModel.trendingTags
|
||||||
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count))
|
||||||
TagRowView(tag: tag)
|
{ tag in
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
TagRowView(tag: tag)
|
||||||
.padding(.vertical, 4)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) {
|
NavigationLink(value: RouterDestination.tagsList(tags: viewModel.trendingTags)) {
|
||||||
Text("see-more")
|
Text("see-more")
|
||||||
.foregroundColor(theme.tintColor)
|
.foregroundColor(theme.tintColor)
|
||||||
|
@ -160,11 +162,12 @@ public struct ExploreView: View {
|
||||||
private var trendingPostsSection: some View {
|
private var trendingPostsSection: some View {
|
||||||
Section("explore.section.trending.posts") {
|
Section("explore.section.trending.posts") {
|
||||||
ForEach(viewModel.trendingStatuses
|
ForEach(viewModel.trendingStatuses
|
||||||
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count))
|
||||||
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
|
{ status in
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) })
|
||||||
.padding(.vertical, 8)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
NavigationLink(value: RouterDestination.trendingTimeline) {
|
NavigationLink(value: RouterDestination.trendingTimeline) {
|
||||||
Text("see-more")
|
Text("see-more")
|
||||||
|
@ -177,11 +180,12 @@ public struct ExploreView: View {
|
||||||
private var trendingLinksSection: some View {
|
private var trendingLinksSection: some View {
|
||||||
Section("explore.section.trending.links") {
|
Section("explore.section.trending.links") {
|
||||||
ForEach(viewModel.trendingLinks
|
ForEach(viewModel.trendingLinks
|
||||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count))
|
||||||
StatusRowCardView(card: card)
|
{ card in
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
StatusRowCardView(card: card)
|
||||||
.padding(.vertical, 8)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
}
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.trendingLinks) { card in
|
ForEach(viewModel.trendingLinks) { card in
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import SwiftUI
|
|
||||||
import Models
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Models
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public struct TagsListView: View {
|
public struct TagsListView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
let tags: [Tag]
|
let tags: [Tag]
|
||||||
|
|
||||||
public init(tags: [Tag]) {
|
public init(tags: [Tag]) {
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(tags) { tag in
|
ForEach(tags) { tag in
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Foundation
|
||||||
|
|
||||||
public struct ServerError: Decodable, Error {
|
public struct ServerError: Decodable, Error {
|
||||||
public let error: String?
|
public let error: String?
|
||||||
|
public var httpCode: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ServerError: Sendable {}
|
extension ServerError: Sendable {}
|
||||||
|
|
|
@ -22,14 +22,14 @@ public struct ServerFilter: Codable, Identifiable, Hashable, Sendable {
|
||||||
public let context: [Context]
|
public let context: [Context]
|
||||||
public let expiresIn: Int?
|
public let expiresIn: Int?
|
||||||
public let expiresAt: ServerDate?
|
public let expiresAt: ServerDate?
|
||||||
|
|
||||||
public func hasExpiry() -> Bool {
|
public func hasExpiry() -> Bool {
|
||||||
return expiresAt != nil
|
return expiresAt != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func isExpired() -> Bool {
|
public func isExpired() -> Bool {
|
||||||
if let expiresAtDate = expiresAt?.asDate {
|
if let expiresAtDate = expiresAt?.asDate {
|
||||||
return expiresAtDate < Date()
|
return expiresAtDate < Date()
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,6 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
public let language: String?
|
public let language: String?
|
||||||
|
|
||||||
|
|
||||||
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.content = content
|
self.content = content
|
||||||
|
@ -277,5 +276,3 @@ extension Status: Sendable {}
|
||||||
|
|
||||||
// Every property in ReblogStatus is immutable.
|
// Every property in ReblogStatus is immutable.
|
||||||
extension ReblogStatus: Sendable {}
|
extension ReblogStatus: Sendable {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
|
||||||
import os
|
import os
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
public static func == (lhs: Client, rhs: Client) -> Bool {
|
public static func == (lhs: Client, rhs: Client) -> Bool {
|
||||||
|
@ -61,7 +61,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) {
|
public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) {
|
||||||
self.server = server
|
self.server = server
|
||||||
self.version = version
|
self.version = version
|
||||||
self.critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server]))
|
critical = .init(initialState: Critical(oauthToken: oauthToken, connections: [server]))
|
||||||
urlSession = URLSession.shared
|
urlSession = URLSession.shared
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
linkHandler = .init(rawLink: link)
|
linkHandler = .init(rawLink: link)
|
||||||
}
|
}
|
||||||
logResponseOnError(httpResponse: httpResponse, data: data)
|
logResponseOnError(httpResponse: httpResponse, data: data)
|
||||||
return (try decoder.decode(Entity.self, from: data), linkHandler)
|
return try (decoder.decode(Entity.self, from: data), linkHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func post<Entity: Decodable>(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity {
|
public func post<Entity: Decodable>(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity {
|
||||||
|
@ -184,7 +184,10 @@ public final class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
do {
|
do {
|
||||||
return try decoder.decode(Entity.self, from: data)
|
return try decoder.decode(Entity.self, from: data)
|
||||||
} catch {
|
} catch {
|
||||||
if let serverError = try? decoder.decode(ServerError.self, from: data) {
|
if var serverError = try? decoder.decode(ServerError.self, from: data) {
|
||||||
|
if let httpResponse = httpResponse as? HTTPURLResponse {
|
||||||
|
serverError.httpCode = httpResponse.statusCode
|
||||||
|
}
|
||||||
throw serverError
|
throw serverError
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
|
|
|
@ -13,7 +13,7 @@ public enum Oauth: Endpoint {
|
||||||
return "oauth/token"
|
return "oauth/token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var jsonValue: Encodable? {
|
public var jsonValue: Encodable? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .token(code, clientId, clientSecret):
|
case let .token(code, clientId, clientSecret):
|
||||||
|
@ -22,7 +22,7 @@ public enum Oauth: Endpoint {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct TokenData: Encodable {
|
public struct TokenData: Encodable {
|
||||||
public let grantType = "authorization_code"
|
public let grantType = "authorization_code"
|
||||||
public let clientId: String
|
public let clientId: String
|
||||||
|
|
|
@ -30,28 +30,28 @@ public struct OpenAIClient {
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
return decoder
|
return decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ChatRequest: OpenAIRequest {
|
public struct ChatRequest: OpenAIRequest {
|
||||||
public struct Message: Encodable {
|
public struct Message: Encodable {
|
||||||
public let role = "user"
|
public let role = "user"
|
||||||
public let content: String
|
public let content: String
|
||||||
}
|
}
|
||||||
|
|
||||||
let model = "gpt-3.5-turbo"
|
let model = "gpt-3.5-turbo"
|
||||||
let messages: [Message]
|
let messages: [Message]
|
||||||
|
|
||||||
let temperature: CGFloat
|
let temperature: CGFloat
|
||||||
|
|
||||||
var path: String {
|
var path: String {
|
||||||
"chat/completions"
|
"chat/completions"
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(content: String, temperature: CGFloat) {
|
public init(content: String, temperature: CGFloat) {
|
||||||
self.messages = [.init(content: content)]
|
messages = [.init(content: content)]
|
||||||
self.temperature = temperature
|
self.temperature = temperature
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Prompt {
|
public enum Prompt {
|
||||||
case correct(input: String)
|
case correct(input: String)
|
||||||
case shorten(input: String)
|
case shorten(input: String)
|
||||||
|
@ -81,7 +81,7 @@ public struct OpenAIClient {
|
||||||
public let role: String
|
public let role: String
|
||||||
public let content: String
|
public let content: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public let message: Message?
|
public let message: Message?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,23 +47,23 @@ extension Models.Notification.NotificationType {
|
||||||
|
|
||||||
func icon(isPrivate: Bool) -> Image {
|
func icon(isPrivate: Bool) -> Image {
|
||||||
if isPrivate {
|
if isPrivate {
|
||||||
return Image(systemName:"tray.fill")
|
return Image(systemName: "tray.fill")
|
||||||
}
|
}
|
||||||
switch self {
|
switch self {
|
||||||
case .status:
|
case .status:
|
||||||
return Image(systemName:"pencil")
|
return Image(systemName: "pencil")
|
||||||
case .mention:
|
case .mention:
|
||||||
return Image(systemName:"at")
|
return Image(systemName: "at")
|
||||||
case .reblog:
|
case .reblog:
|
||||||
return Image("Rocket.Fill")
|
return Image("Rocket.Fill")
|
||||||
case .follow, .follow_request:
|
case .follow, .follow_request:
|
||||||
return Image(systemName:"person.fill.badge.plus")
|
return Image(systemName: "person.fill.badge.plus")
|
||||||
case .favourite:
|
case .favourite:
|
||||||
return Image(systemName:"star.fill")
|
return Image(systemName: "star.fill")
|
||||||
case .poll:
|
case .poll:
|
||||||
return Image(systemName:"chart.bar.fill")
|
return Image(systemName: "chart.bar.fill")
|
||||||
case .update:
|
case .update:
|
||||||
return Image(systemName:"pencil.line")
|
return Image(systemName: "pencil.line")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,8 @@ public struct NotificationsListView: View {
|
||||||
case .error:
|
case .error:
|
||||||
ErrorView(title: "notifications.error.title",
|
ErrorView(title: "notifications.error.title",
|
||||||
message: "notifications.error.message",
|
message: "notifications.error.message",
|
||||||
buttonTitle: "action.retry") {
|
buttonTitle: "action.retry")
|
||||||
|
{
|
||||||
Task {
|
Task {
|
||||||
await viewModel.fetchNotifications()
|
await viewModel.fetchNotifications()
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,7 +155,8 @@ public struct StatusDetailView: View {
|
||||||
private var errorView: some View {
|
private var errorView: some View {
|
||||||
ErrorView(title: "status.error.title",
|
ErrorView(title: "status.error.title",
|
||||||
message: "status.error.message",
|
message: "status.error.message",
|
||||||
buttonTitle: "action.retry") {
|
buttonTitle: "action.retry")
|
||||||
|
{
|
||||||
Task {
|
Task {
|
||||||
await viewModel.fetch()
|
await viewModel.fetch()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import Env
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Env
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class StatusDetailViewModel: ObservableObject {
|
class StatusDetailViewModel: ObservableObject {
|
||||||
|
@ -79,9 +79,9 @@ class StatusDetailViewModel: ObservableObject {
|
||||||
var statuses = data.context.ancestors
|
var statuses = data.context.ancestors
|
||||||
statuses.append(data.status)
|
statuses.append(data.status)
|
||||||
statuses.append(contentsOf: data.context.descendants)
|
statuses.append(contentsOf: data.context.descendants)
|
||||||
|
|
||||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||||
|
|
||||||
if animate {
|
if animate {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isLoadingContext = false
|
isLoadingContext = false
|
||||||
|
|
|
@ -51,14 +51,14 @@ struct StatusEditorAccessoryView: View {
|
||||||
matching: .any(of: [.images, .videos]))
|
matching: .any(of: [.images, .videos]))
|
||||||
.fileImporter(isPresented: $isFileImporterPresented,
|
.fileImporter(isPresented: $isFileImporterPresented,
|
||||||
allowedContentTypes: [.image, .video],
|
allowedContentTypes: [.image, .video],
|
||||||
allowsMultipleSelection: true) { result in
|
allowsMultipleSelection: true)
|
||||||
|
{ result in
|
||||||
if let urls = try? result.get() {
|
if let urls = try? result.get() {
|
||||||
viewModel.processURLs(urls: urls)
|
viewModel.processURLs(urls: urls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||||
.disabled(viewModel.showPoll)
|
.disabled(viewModel.showPoll)
|
||||||
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
|
|
|
@ -1,86 +1,87 @@
|
||||||
|
import AVFoundation
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
actor StatusEditorCompressor {
|
actor StatusEditorCompressor {
|
||||||
enum CompressorError: Error {
|
enum CompressorError: Error {
|
||||||
case noData
|
case noData
|
||||||
}
|
}
|
||||||
|
|
||||||
func compressImageFrom(url: URL) async -> Data? {
|
func compressImageFrom(url: URL) async -> Data? {
|
||||||
return await withCheckedContinuation{ continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
||||||
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
|
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
|
||||||
continuation.resume(returning: nil)
|
continuation.resume(returning: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxPixelSize: Int
|
let maxPixelSize: Int
|
||||||
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||||
maxPixelSize = 1536
|
maxPixelSize = 1536
|
||||||
} else {
|
} else {
|
||||||
maxPixelSize = 4096
|
maxPixelSize = 4096
|
||||||
}
|
}
|
||||||
|
|
||||||
let downsampleOptions = [
|
let downsampleOptions = [
|
||||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||||
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
|
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
|
||||||
] as [CFString : Any] as CFDictionary
|
] as [CFString: Any] as CFDictionary
|
||||||
|
|
||||||
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else {
|
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else {
|
||||||
continuation.resume(returning: nil)
|
continuation.resume(returning: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = NSMutableData()
|
let data = NSMutableData()
|
||||||
guard let imageDestination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
|
guard let imageDestination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
|
||||||
continuation.resume(returning: nil)
|
continuation.resume(returning: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let isPNG: Bool = {
|
let isPNG: Bool = {
|
||||||
guard let utType = cgImage.utType else { return false }
|
guard let utType = cgImage.utType else { return false }
|
||||||
return (utType as String) == UTType.png.identifier
|
return (utType as String) == UTType.png.identifier
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let destinationProperties = [
|
let destinationProperties = [
|
||||||
kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75
|
kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75,
|
||||||
] as CFDictionary
|
] as CFDictionary
|
||||||
|
|
||||||
CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties)
|
CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties)
|
||||||
CGImageDestinationFinalize(imageDestination)
|
CGImageDestinationFinalize(imageDestination)
|
||||||
|
|
||||||
continuation.resume(returning: data as Data)
|
continuation.resume(returning: data as Data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compressImageForUpload(_ image: UIImage) async throws -> Data {
|
func compressImageForUpload(_ image: UIImage) async throws -> Data {
|
||||||
var image = image
|
var image = image
|
||||||
if image.size.height > 5000 || image.size.width > 5000 {
|
if image.size.height > 5000 || image.size.width > 5000 {
|
||||||
image = image.resized(to: .init(width: image.size.width / 4,
|
image = image.resized(to: .init(width: image.size.width / 4,
|
||||||
height: image.size.height / 4))
|
height: image.size.height / 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard var imageData = image.jpegData(compressionQuality: 0.8) else {
|
guard var imageData = image.jpegData(compressionQuality: 0.8) else {
|
||||||
throw CompressorError.noData
|
throw CompressorError.noData
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxSize: Int = 10 * 1024 * 1024
|
let maxSize = 10 * 1024 * 1024
|
||||||
|
|
||||||
if imageData.count > maxSize {
|
if imageData.count > maxSize {
|
||||||
while imageData.count > maxSize {
|
while imageData.count > maxSize {
|
||||||
guard let compressedImage = UIImage(data: imageData),
|
guard let compressedImage = UIImage(data: imageData),
|
||||||
let compressedData = compressedImage.jpegData(compressionQuality: 0.8) else {
|
let compressedData = compressedImage.jpegData(compressionQuality: 0.8)
|
||||||
|
else {
|
||||||
throw CompressorError.noData
|
throw CompressorError.noData
|
||||||
}
|
}
|
||||||
imageData = compressedData
|
imageData = compressedData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageData
|
return imageData
|
||||||
}
|
}
|
||||||
|
|
||||||
func compressVideo(_ url: URL) async -> URL? {
|
func compressVideo(_ url: URL) async -> URL? {
|
||||||
await withCheckedContinuation { continuation in
|
await withCheckedContinuation { continuation in
|
||||||
let urlAsset = AVURLAsset(url: url, options: nil)
|
let urlAsset = AVURLAsset(url: url, options: nil)
|
||||||
|
@ -97,5 +98,4 @@ actor StatusEditorCompressor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,11 +31,11 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
// Main actor-isolated static property 'allCases' cannot be used to
|
// Main actor-isolated static property 'allCases' cannot be used to
|
||||||
// satisfy nonisolated protocol requirement
|
// satisfy nonisolated protocol requirement
|
||||||
//
|
//
|
||||||
nonisolated public static var allCases: [StatusEditorUTTypeSupported] {
|
public nonisolated static var allCases: [StatusEditorUTTypeSupported] {
|
||||||
[.url, .text, .plaintext, .image, .jpeg, .png, .tiff, .video,
|
[.url, .text, .plaintext, .image, .jpeg, .png, .tiff, .video,
|
||||||
.movie, .mp4, .gif, .gif2, .quickTimeMovie, .uiimage, .adobeRawImage]
|
.movie, .mp4, .gif, .gif2, .quickTimeMovie, .uiimage, .adobeRawImage]
|
||||||
}
|
}
|
||||||
|
|
||||||
static func types() -> [UTType] {
|
static func types() -> [UTType] {
|
||||||
[.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
|
[.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@ public struct StatusEditorView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var languageConfirmationDialog: some View {
|
private var languageConfirmationDialog: some View {
|
||||||
if let (detected: detected, selected: selected) = viewModel.languageConfirmationDialogLanguages,
|
if let (detected: detected, selected: selected) = viewModel.languageConfirmationDialogLanguages,
|
||||||
let detectedLong = Locale.current.localizedString(forLanguageCode: detected),
|
let detectedLong = Locale.current.localizedString(forLanguageCode: detected),
|
||||||
let selectedLong = Locale.current.localizedString(forLanguageCode: selected)
|
let selectedLong = Locale.current.localizedString(forLanguageCode: selected)
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,6 +20,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var theme: Theme?
|
var theme: Theme?
|
||||||
var preferences: UserPreferences?
|
var preferences: UserPreferences?
|
||||||
var languageConfirmationDialogLanguages: (detected: String, selected: String)?
|
var languageConfirmationDialogLanguages: (detected: String, selected: String)?
|
||||||
|
@ -69,7 +70,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||||
var statusTextCharacterLength: Int {
|
var statusTextCharacterLength: Int {
|
||||||
urlLengthAdjustments - statusText.string.utf16.count - spoilerTextCount
|
urlLengthAdjustments - statusText.string.utf16.count - spoilerTextCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private var itemsProvider: [NSItemProvider]?
|
private var itemsProvider: [NSItemProvider]?
|
||||||
|
|
||||||
@Published var backupStatusText: NSAttributedString?
|
@Published var backupStatusText: NSAttributedString?
|
||||||
|
@ -135,7 +136,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mentionString: String?
|
private var mentionString: String?
|
||||||
|
|
||||||
private var uploadTask: Task<Void, Never>?
|
private var uploadTask: Task<Void, Never>?
|
||||||
private var suggestedTask: Task<Void, Never>?
|
private var suggestedTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
@ -363,11 +364,11 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Shar sheet / Item provider
|
// MARK: - Shar sheet / Item provider
|
||||||
|
|
||||||
func processURLs(urls: [URL]) {
|
func processURLs(urls: [URL]) {
|
||||||
isMediasLoading = true
|
isMediasLoading = true
|
||||||
let items = urls.filter { $0.startAccessingSecurityScopedResource() }
|
let items = urls.filter { $0.startAccessingSecurityScopedResource() }
|
||||||
.compactMap { NSItemProvider(contentsOf: $0) }
|
.compactMap { NSItemProvider(contentsOf: $0) }
|
||||||
processItemsProvider(items: items)
|
processItemsProvider(items: items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,7 +392,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||||
error: nil))
|
error: nil))
|
||||||
} else if let content = content as? ImageFileTranseferable,
|
} else if let content = content as? ImageFileTranseferable,
|
||||||
let compressedData = await compressor.compressImageFrom(url: content.url),
|
let compressedData = await compressor.compressImageFrom(url: content.url),
|
||||||
let image = UIImage(data: compressedData) {
|
let image = UIImage(data: compressedData)
|
||||||
|
{
|
||||||
mediasImages.append(.init(image: image,
|
mediasImages.append(.init(image: image,
|
||||||
movieTransferable: nil,
|
movieTransferable: nil,
|
||||||
gifTransferable: nil,
|
gifTransferable: nil,
|
||||||
|
@ -616,7 +618,8 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
} else if let videoURL = originalContainer.movieTransferable?.url,
|
} else if let videoURL = originalContainer.movieTransferable?.url,
|
||||||
let compressedVideoURL = await compressor.compressVideo(videoURL),
|
let compressedVideoURL = await compressor.compressVideo(videoURL),
|
||||||
let data = try? Data(contentsOf: compressedVideoURL) {
|
let data = try? Data(contentsOf: compressedVideoURL)
|
||||||
|
{
|
||||||
let uploadedMedia = try await uploadMedia(data: data, mimeType: compressedVideoURL.mimeType())
|
let uploadedMedia = try await uploadMedia(data: data, mimeType: compressedVideoURL.mimeType())
|
||||||
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
||||||
movieTransferable: originalContainer.movieTransferable,
|
movieTransferable: originalContainer.movieTransferable,
|
||||||
|
|
|
@ -7,22 +7,23 @@ import SwiftUI
|
||||||
|
|
||||||
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@ObservedObject private var fetcher: Fetcher
|
@ObservedObject private var fetcher: Fetcher
|
||||||
private let isRemote: Bool
|
private let isRemote: Bool
|
||||||
private let routerPath: RouterPath
|
private let routerPath: RouterPath
|
||||||
private let client: Client
|
private let client: Client
|
||||||
|
|
||||||
public init(fetcher: Fetcher,
|
public init(fetcher: Fetcher,
|
||||||
client: Client,
|
client: Client,
|
||||||
routerPath: RouterPath,
|
routerPath: RouterPath,
|
||||||
isRemote: Bool = false) {
|
isRemote: Bool = false)
|
||||||
|
{
|
||||||
self.fetcher = fetcher
|
self.fetcher = fetcher
|
||||||
self.isRemote = isRemote
|
self.isRemote = isRemote
|
||||||
self.client = client
|
self.client = client
|
||||||
self.routerPath = routerPath
|
self.routerPath = routerPath
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
switch fetcher.statusesState {
|
switch fetcher.statusesState {
|
||||||
case .loading:
|
case .loading:
|
||||||
|
@ -33,29 +34,30 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
case .error:
|
case .error:
|
||||||
ErrorView(title: "status.error.title",
|
ErrorView(title: "status.error.title",
|
||||||
message: "status.error.loading.message",
|
message: "status.error.loading.message",
|
||||||
buttonTitle: "action.retry") {
|
buttonTitle: "action.retry")
|
||||||
|
{
|
||||||
Task {
|
Task {
|
||||||
await fetcher.fetchNewestStatuses()
|
await fetcher.fetchNewestStatuses()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
|
||||||
case let .display(statuses, nextPageState):
|
case let .display(statuses, nextPageState):
|
||||||
ForEach(statuses, id: \.viewId) { status in
|
ForEach(statuses, id: \.viewId) { status in
|
||||||
StatusRowView(viewModel: { StatusRowViewModel(status: status,
|
StatusRowView(viewModel: { StatusRowViewModel(status: status,
|
||||||
client: client,
|
client: client,
|
||||||
routerPath: routerPath,
|
routerPath: routerPath,
|
||||||
isRemote: isRemote)
|
isRemote: isRemote)
|
||||||
|
|
||||||
})
|
})
|
||||||
.id(status.id)
|
.id(status.id)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
fetcher.statusDidAppear(status: status)
|
fetcher.statusDidAppear(status: status)
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
fetcher.statusDidDisappear(status: status)
|
fetcher.statusDidDisappear(status: status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch nextPageState {
|
switch nextPageState {
|
||||||
case .hasNextPage:
|
case .hasNextPage:
|
||||||
|
@ -72,7 +74,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingRow: some View {
|
private var loadingRow: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -22,7 +22,8 @@ class VideoPlayerViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
guard let player else { return }
|
guard let player else { return }
|
||||||
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
|
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
|
||||||
object: player.currentItem, queue: .main) { [weak self] _ in
|
object: player.currentItem, queue: .main)
|
||||||
|
{ [weak self] _ in
|
||||||
if autoPlay {
|
if autoPlay {
|
||||||
self?.player?.seek(to: CMTime.zero)
|
self?.player?.seek(to: CMTime.zero)
|
||||||
self?.player?.play()
|
self?.player?.play()
|
||||||
|
|
|
@ -10,20 +10,20 @@ public struct StatusRowView: View {
|
||||||
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
@Environment(\.isCompact) private var isCompact: Bool
|
@Environment(\.isCompact) private var isCompact: Bool
|
||||||
|
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@StateObject var viewModel: StatusRowViewModel
|
@StateObject var viewModel: StatusRowViewModel
|
||||||
|
|
||||||
// StateObject accepts an @autoclosure which only allocates the view model once when the view gets on screen.
|
// StateObject accepts an @autoclosure which only allocates the view model once when the view gets on screen.
|
||||||
public init(viewModel: @escaping () -> StatusRowViewModel) {
|
public init(viewModel: @escaping () -> StatusRowViewModel) {
|
||||||
_viewModel = StateObject(wrappedValue: viewModel())
|
_viewModel = StateObject(wrappedValue: viewModel())
|
||||||
}
|
}
|
||||||
|
|
||||||
var contextMenu: some View {
|
var contextMenu: some View {
|
||||||
StatusRowContextMenu(viewModel: viewModel)
|
StatusRowContextMenu(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if viewModel.isFiltered, let filter = viewModel.filter {
|
if viewModel.isFiltered, let filter = viewModel.filter {
|
||||||
|
@ -151,7 +151,7 @@ public struct StatusRowView: View {
|
||||||
client: viewModel.client)
|
client: viewModel.client)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var accesibilityActions: some View {
|
private var accesibilityActions: some View {
|
||||||
// Add the individual mentions as accessibility actions
|
// Add the individual mentions as accessibility actions
|
||||||
|
@ -160,20 +160,20 @@ public struct StatusRowView: View {
|
||||||
viewModel.routerPath.navigate(to: .accountDetail(id: mention.id))
|
viewModel.routerPath.navigate(to: .accountDetail(id: mention.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") {
|
Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.displaySpoiler.toggle()
|
viewModel.displaySpoiler.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("@\(viewModel.status.account.username)") {
|
Button("@\(viewModel.status.account.username)") {
|
||||||
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id))
|
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMenu
|
contextMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeFilterView(filter: Filter) -> some View {
|
private func makeFilterView(filter: Filter) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("status.filter.filtered-by-\(filter.title)")
|
Text("status.filter.filtered-by-\(filter.title)")
|
||||||
|
@ -186,7 +186,7 @@ public struct StatusRowView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var remoteContentLoadingView: some View {
|
private var remoteContentLoadingView: some View {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import Combine
|
import Combine
|
||||||
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import NaturalLanguage
|
import NaturalLanguage
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import DesignSystem
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class StatusRowViewModel: ObservableObject {
|
public class StatusRowViewModel: ObservableObject {
|
||||||
|
@ -39,25 +39,26 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
recalcCollapse()
|
recalcCollapse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// number of lines to show, nil means show the whole post
|
// number of lines to show, nil means show the whole post
|
||||||
@Published var lineLimit: Int? = nil
|
@Published var lineLimit: Int? = nil
|
||||||
// post length determining if the post should be collapsed
|
// post length determining if the post should be collapsed
|
||||||
let collapseThresholdLength : Int = 750
|
let collapseThresholdLength: Int = 750
|
||||||
// number of text lines to show on a collpased post
|
// number of text lines to show on a collpased post
|
||||||
let collapsedLines: Int = 8
|
let collapsedLines: Int = 8
|
||||||
// user preference, set in init
|
// user preference, set in init
|
||||||
var collapseLongPosts: Bool = false
|
var collapseLongPosts: Bool = false
|
||||||
|
|
||||||
private func recalcCollapse() {
|
private func recalcCollapse() {
|
||||||
let hasContentWarning = !status.spoilerText.asRawText.isEmpty
|
let hasContentWarning = !status.spoilerText.asRawText.isEmpty
|
||||||
let showCollapseButton = collapseLongPosts && isCollapsed && !hasContentWarning
|
let showCollapseButton = collapseLongPosts && isCollapsed && !hasContentWarning
|
||||||
&& finalStatus.content.asRawText.unicodeScalars.count > collapseThresholdLength
|
&& finalStatus.content.asRawText.unicodeScalars.count > collapseThresholdLength
|
||||||
let newlineLimit = showCollapseButton && isCollapsed ? collapsedLines : nil
|
let newlineLimit = showCollapseButton && isCollapsed ? collapsedLines : nil
|
||||||
if newlineLimit != lineLimit {
|
if newlineLimit != lineLimit {
|
||||||
lineLimit = newlineLimit
|
lineLimit = newlineLimit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let theme = Theme.shared
|
private let theme = Theme.shared
|
||||||
private let userMentionned: Bool
|
private let userMentionned: Bool
|
||||||
|
|
||||||
|
@ -94,7 +95,7 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
textDisabled: Bool = false)
|
textDisabled: Bool = false)
|
||||||
{
|
{
|
||||||
self.status = status
|
self.status = status
|
||||||
self.finalStatus = status.reblog ?? status
|
finalStatus = status.reblog ?? status
|
||||||
self.client = client
|
self.client = client
|
||||||
self.routerPath = routerPath
|
self.routerPath = routerPath
|
||||||
self.isFocused = isFocused
|
self.isFocused = isFocused
|
||||||
|
@ -112,13 +113,12 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
displaySpoiler = !finalStatus.spoilerText.asRawText.isEmpty
|
displaySpoiler = !finalStatus.spoilerText.asRawText.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if status.mentions.first(where: { $0.id == CurrentAccount.shared.account?.id }) != nil {
|
if status.mentions.first(where: { $0.id == CurrentAccount.shared.account?.id }) != nil {
|
||||||
userMentionned = true
|
userMentionned = true
|
||||||
} else {
|
} else {
|
||||||
userMentionned = false
|
userMentionned = false
|
||||||
}
|
}
|
||||||
|
|
||||||
isFiltered = filter != nil
|
isFiltered = filter != nil
|
||||||
|
|
||||||
if let url = embededStatusURL(),
|
if let url = embededStatusURL(),
|
||||||
|
@ -127,7 +127,7 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
isEmbedLoading = false
|
isEmbedLoading = false
|
||||||
embeddedStatus = embed
|
embeddedStatus = embed
|
||||||
}
|
}
|
||||||
|
|
||||||
collapseLongPosts = UserPreferences.shared.collapseLongPosts
|
collapseLongPosts = UserPreferences.shared.collapseLongPosts
|
||||||
recalcCollapse()
|
recalcCollapse()
|
||||||
}
|
}
|
||||||
|
@ -187,7 +187,8 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
if !content.statusesURLs.isEmpty,
|
if !content.statusesURLs.isEmpty,
|
||||||
let url = content.statusesURLs.first,
|
let url = content.statusesURLs.first,
|
||||||
!StatusEmbedCache.shared.badStatusesURLs.contains(url),
|
!StatusEmbedCache.shared.badStatusesURLs.contains(url),
|
||||||
client.hasConnection(with: url) {
|
client.hasConnection(with: url)
|
||||||
|
{
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -202,7 +203,7 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let embed = StatusEmbedCache.shared.get(url: url) {
|
if let embed = StatusEmbedCache.shared.get(url: url) {
|
||||||
isEmbedLoading = false
|
isEmbedLoading = false
|
||||||
embeddedStatus = embed
|
embeddedStatus = embed
|
||||||
|
@ -224,8 +225,7 @@ public class StatusRowViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
if let embed {
|
if let embed {
|
||||||
StatusEmbedCache.shared.set(url: url, status: embed)
|
StatusEmbedCache.shared.set(url: url, status: embed)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
StatusEmbedCache.shared.badStatusesURLs.insert(url)
|
StatusEmbedCache.shared.badStatusesURLs.insert(url)
|
||||||
}
|
}
|
||||||
withAnimation {
|
withAnimation {
|
||||||
|
|
|
@ -24,7 +24,7 @@ struct StatusRowActionsView: View {
|
||||||
// Main actor-isolated static property 'allCases' cannot be used to
|
// Main actor-isolated static property 'allCases' cannot be used to
|
||||||
// satisfy nonisolated protocol requirement
|
// satisfy nonisolated protocol requirement
|
||||||
//
|
//
|
||||||
nonisolated public static var allCases: [StatusRowActionsView.Action] {
|
public nonisolated static var allCases: [StatusRowActionsView.Action] {
|
||||||
[.respond, .boost, .favorite, .bookmark, .share]
|
[.respond, .boost, .favorite, .bookmark, .share]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,8 @@ struct StatusRowActionsView: View {
|
||||||
{
|
{
|
||||||
ShareLink(item: url,
|
ShareLink(item: url,
|
||||||
subject: Text(viewModel.finalStatus.account.safeDisplayName),
|
subject: Text(viewModel.finalStatus.account.safeDisplayName),
|
||||||
message: Text(viewModel.finalStatus.content.asRawText)) {
|
message: Text(viewModel.finalStatus.content.asRawText))
|
||||||
|
{
|
||||||
action.image(dataController: statusDataController)
|
action.image(dataController: statusDataController)
|
||||||
}
|
}
|
||||||
.buttonStyle(.statusAction())
|
.buttonStyle(.statusAction())
|
||||||
|
@ -142,7 +143,8 @@ struct StatusRowActionsView: View {
|
||||||
(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id))
|
(viewModel.status.visibility == .direct || viewModel.status.visibility == .priv && viewModel.status.account.id != currentAccount.account?.id))
|
||||||
if let count = action.count(dataController: statusDataController,
|
if let count = action.count(dataController: statusDataController,
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
theme: theme), !viewModel.isRemote {
|
theme: theme), !viewModel.isRemote
|
||||||
|
{
|
||||||
Text("\(count)")
|
Text("\(count)")
|
||||||
.foregroundColor(Color(UIColor.secondaryLabel))
|
.foregroundColor(Color(UIColor.secondaryLabel))
|
||||||
.font(.scaledFootnote)
|
.font(.scaledFootnote)
|
||||||
|
@ -150,7 +152,6 @@ struct StatusRowActionsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func handleAction(action: Action) {
|
private func handleAction(action: Action) {
|
||||||
Task {
|
Task {
|
||||||
|
|
|
@ -71,7 +71,8 @@ struct StatusRowContextMenu: View {
|
||||||
{
|
{
|
||||||
ShareLink(item: url,
|
ShareLink(item: url,
|
||||||
subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName),
|
subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName),
|
||||||
message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText)) {
|
message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText))
|
||||||
|
{
|
||||||
Label("status.action.share", systemImage: "square.and.arrow.up")
|
Label("status.action.share", systemImage: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct StatusRowDetailView: View {
|
struct StatusRowDetailView: View {
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
|
||||||
@EnvironmentObject private var statusDataController: StatusDataController
|
@EnvironmentObject private var statusDataController: StatusDataController
|
||||||
|
|
||||||
@ObservedObject var viewModel: StatusRowViewModel
|
@ObservedObject var viewModel: StatusRowViewModel
|
||||||
|
|
|
@ -124,7 +124,8 @@ public struct StatusRowMediaPreviewView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("status.editor.media.image-description",
|
.alert("status.editor.media.image-description",
|
||||||
isPresented: $isAltAlertDisplayed) {
|
isPresented: $isAltAlertDisplayed)
|
||||||
|
{
|
||||||
Button("alert.button.ok", action: {})
|
Button("alert.button.ok", action: {})
|
||||||
} message: {
|
} message: {
|
||||||
Text(altTextDisplayed ?? "")
|
Text(altTextDisplayed ?? "")
|
||||||
|
|
|
@ -109,9 +109,9 @@ struct StatusRowSwipeView: View {
|
||||||
isBookmarked: statusDataController.isBookmarked,
|
isBookmarked: statusDataController.isBookmarked,
|
||||||
privateBoost: privateBoost),
|
privateBoost: privateBoost),
|
||||||
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
|
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
|
||||||
isFavorited: statusDataController.isFavorited,
|
isFavorited: statusDataController.isFavorited,
|
||||||
isBookmarked: statusDataController.isBookmarked,
|
isBookmarked: statusDataController.isBookmarked,
|
||||||
privateBoost: privateBoost))
|
privateBoost: privateBoost))
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
.environment(\.symbolVariants, .none)
|
.environment(\.symbolVariants, .none)
|
||||||
case .iconWithText:
|
case .iconWithText:
|
||||||
|
@ -120,9 +120,9 @@ struct StatusRowSwipeView: View {
|
||||||
isBookmarked: statusDataController.isBookmarked,
|
isBookmarked: statusDataController.isBookmarked,
|
||||||
privateBoost: privateBoost),
|
privateBoost: privateBoost),
|
||||||
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
|
imageNamed: action.iconName(isReblogged: statusDataController.isReblogged,
|
||||||
isFavorited: statusDataController.isFavorited,
|
isFavorited: statusDataController.isFavorited,
|
||||||
isBookmarked: statusDataController.isBookmarked,
|
isBookmarked: statusDataController.isBookmarked,
|
||||||
privateBoost: privateBoost))
|
privateBoost: privateBoost))
|
||||||
.labelStyle(.titleAndIcon)
|
.labelStyle(.titleAndIcon)
|
||||||
.environment(\.symbolVariants, .none)
|
.environment(\.symbolVariants, .none)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ struct StatusRowTextView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
@ObservedObject var viewModel: StatusRowViewModel
|
@ObservedObject var viewModel: StatusRowViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
@ -34,7 +34,7 @@ public actor TimelineCache {
|
||||||
try await engine.removeAllData()
|
try await engine.removeAllData()
|
||||||
let itemKeys = statuses.map { CacheKey($0[keyPath: \.id]) }
|
let itemKeys = statuses.map { CacheKey($0[keyPath: \.id]) }
|
||||||
let dataAndKeys = try zip(itemKeys, statuses)
|
let dataAndKeys = try zip(itemKeys, statuses)
|
||||||
.map { (key: $0, data: try encoder.encode($1)) }
|
.map { try (key: $0, data: encoder.encode($1)) }
|
||||||
try await engine.write(dataAndKeys)
|
try await engine.write(dataAndKeys)
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ actor TimelineDatasource {
|
||||||
}
|
}
|
||||||
|
|
||||||
func get() -> [Status] {
|
func get() -> [Status] {
|
||||||
statuses.filter{ $0.filtered?.first?.filter.filterAction != .hide }
|
statuses.filter { $0.filtered?.first?.filter.filterAction != .hide }
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
|
|
|
@ -162,7 +162,7 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
}
|
}
|
||||||
await fetchNewestStatuses()
|
await fetchNewestStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNewestStatuses() async {
|
func fetchNewestStatuses() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
|
|
Loading…
Reference in a new issue