mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-22 21:08:15 +01:00
Run swiftformat
This commit is contained in:
parent
5951bcec38
commit
a2fe0511e0
@ -83,8 +83,8 @@ extension View {
|
||||
AddRemoteTimelineView()
|
||||
.withEnvironments()
|
||||
case .addTagGroup:
|
||||
AddTagGroupView()
|
||||
.withEnvironments()
|
||||
AddTagGroupView()
|
||||
.withEnvironments()
|
||||
case let .statusEditHistory(status):
|
||||
StatusEditHistoryView(statusId: status)
|
||||
.withEnvironments()
|
||||
|
@ -97,9 +97,9 @@ struct SideBarView<Content: View>: View {
|
||||
private var tabsView: some View {
|
||||
ForEach(tabs) { tab in
|
||||
Button {
|
||||
//ensure keyboard is always dismissed when selecting a tab
|
||||
// ensure keyboard is always dismissed when selecting a tab
|
||||
hideKeyboard()
|
||||
|
||||
|
||||
if tab == selectedTab {
|
||||
popToRootTab = .other
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
@ -176,8 +176,8 @@ private struct SideBarIcon: View {
|
||||
}
|
||||
|
||||
extension View {
|
||||
func hideKeyboard() {
|
||||
let resign = #selector(UIResponder.resignFirstResponder)
|
||||
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
|
||||
}
|
||||
func hideKeyboard() {
|
||||
let resign = #selector(UIResponder.resignFirstResponder)
|
||||
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ struct ContentSettingsView: View {
|
||||
Text("settings.content.media.show.alt")
|
||||
}
|
||||
}.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
||||
Section("settings.content.sharing") {
|
||||
Picker("settings.content.sharing.share-button-behavior", selection: $userPreferences.shareButtonBehavior) {
|
||||
ForEach(PreferredShareButtonBehavior.allCases, id: \.rawValue) { option in
|
||||
|
@ -13,7 +13,7 @@ class DisplaySettingsLocalValues: ObservableObject {
|
||||
@Published var labelColor = Theme.shared.labelColor
|
||||
@Published var lineSpacing = Theme.shared.lineSpacing
|
||||
@Published var fontSizeScale = Theme.shared.fontSizeScale
|
||||
|
||||
|
||||
private let debouncesDelay: DispatchQueue.SchedulerTimeType.Stride = .seconds(0.5)
|
||||
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
|
@ -139,9 +139,9 @@ struct SettingsTabs: View {
|
||||
NavigationLink(destination: remoteLocalTimelinesView) {
|
||||
Label("settings.general.remote-timelines", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
NavigationLink(destination: tagGroupsView) {
|
||||
Label("timeline.filter.tag-groups", systemImage: "number")
|
||||
}
|
||||
NavigationLink(destination: tagGroupsView) {
|
||||
Label("timeline.filter.tag-groups", systemImage: "number")
|
||||
}
|
||||
NavigationLink(destination: ContentSettingsView()) {
|
||||
Label("settings.general.content", systemImage: "rectangle.stack")
|
||||
}
|
||||
@ -264,36 +264,36 @@ struct SettingsTabs: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var tagGroupsView: some View {
|
||||
Form {
|
||||
ForEach(preferences.tagGroups, id: \.self) { group in
|
||||
Text(group.title)
|
||||
}
|
||||
.onDelete { indexes in
|
||||
if let index = indexes.first {
|
||||
_ = preferences.tagGroups.remove(at: index)
|
||||
}
|
||||
}
|
||||
.onMove { source, destination in
|
||||
preferences.tagGroups.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
Button {
|
||||
routerPath.presentedSheet = .addTagGroup
|
||||
} label: {
|
||||
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
private var tagGroupsView: some View {
|
||||
Form {
|
||||
ForEach(preferences.tagGroups, id: \.self) { group in
|
||||
Text(group.title)
|
||||
}
|
||||
.navigationTitle("timeline.filter.tag-groups")
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.toolbar {
|
||||
EditButton()
|
||||
.onDelete { indexes in
|
||||
if let index = indexes.first {
|
||||
_ = preferences.tagGroups.remove(at: index)
|
||||
}
|
||||
}
|
||||
.onMove { source, destination in
|
||||
preferences.tagGroups.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
Button {
|
||||
routerPath.presentedSheet = .addTagGroup
|
||||
} label: {
|
||||
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
.navigationTitle("timeline.filter.tag-groups")
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
|
||||
private var remoteLocalTimelinesView: some View {
|
||||
Form {
|
||||
@ -317,7 +317,7 @@ struct SettingsTabs: View {
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.toolbar {
|
||||
EditButton()
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ struct TranslationSettingsView: View {
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
Toggle(isOn: preferences.$autoDetectPostLanguage) {
|
||||
Text("settings.translation.auto-detect-post-language")
|
||||
@ -46,7 +46,6 @@ struct TranslationSettingsView: View {
|
||||
} footer: {
|
||||
Text("settings.translation.auto-detect-post-language-footer")
|
||||
}
|
||||
|
||||
}
|
||||
.navigationTitle("settings.translation.navigation-title")
|
||||
.scrollContentBackground(.hidden)
|
||||
|
@ -8,190 +8,188 @@ import Shimmer
|
||||
import SwiftUI
|
||||
|
||||
struct AddTagGroupView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@State private var title: String = ""
|
||||
@State private var sfSymbolName: String = ""
|
||||
@State private var tags: [String] = []
|
||||
@State private var newTag: String = ""
|
||||
|
||||
private var canSave: Bool {
|
||||
!title.isEmpty &&
|
||||
// At least have 2 tags, one main and one additional.
|
||||
tags.count >= 2
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: Focus?
|
||||
|
||||
enum Focus {
|
||||
case title
|
||||
case symbol
|
||||
case new
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
metadataSection
|
||||
keywordsSection
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle("timeline.filter.add-tag-groups")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("action.cancel", action: { dismiss() })
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.save", action: { save() })
|
||||
.disabled(!canSave)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
focusedField = .title
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
symbolsSuggestionView
|
||||
}
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject private var preferences: UserPreferences
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
@State private var title: String = ""
|
||||
@State private var sfSymbolName: String = ""
|
||||
@State private var tags: [String] = []
|
||||
@State private var newTag: String = ""
|
||||
|
||||
private var canSave: Bool {
|
||||
!title.isEmpty &&
|
||||
// At least have 2 tags, one main and one additional.
|
||||
tags.count >= 2
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: Focus?
|
||||
|
||||
enum Focus {
|
||||
case title
|
||||
case symbol
|
||||
case new
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
metadataSection
|
||||
keywordsSection
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle("timeline.filter.add-tag-groups")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("action.cancel", action: { dismiss() })
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var metadataSection: some View {
|
||||
Section {
|
||||
TextField("add-tag-groups.edit.title.field", text: $title, axis: .horizontal)
|
||||
.focused($focusedField, equals: Focus.title)
|
||||
.onSubmit {
|
||||
focusedField = Focus.symbol
|
||||
}
|
||||
|
||||
HStack {
|
||||
TextField("add-tag-groups.edit.icon.field", text: $sfSymbolName, axis: .horizontal)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($focusedField, equals: Focus.symbol)
|
||||
.onSubmit {
|
||||
focusedField = Focus.new
|
||||
}
|
||||
.onChange(of: sfSymbolName) { name in
|
||||
popupTagsPresented = true
|
||||
}
|
||||
|
||||
Image(systemName: sfSymbolName)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.save", action: { save() })
|
||||
.disabled(!canSave)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
focusedField = .title
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
symbolsSuggestionView
|
||||
}
|
||||
}
|
||||
|
||||
@State private var popupTagsPresented = false
|
||||
|
||||
private var keywordsSection: some View {
|
||||
Section("add-tag-groups.edit.tags") {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
HStack {
|
||||
Text(tag)
|
||||
Spacer()
|
||||
Button {
|
||||
deleteTag(tag)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { indexes in
|
||||
if let index = indexes.first {
|
||||
let tag = tags[index]
|
||||
deleteTag(tag)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
TextField("add-tag-groups.edit.tags.add", text: $newTag, axis: .horizontal)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.onSubmit {
|
||||
addNewTag()
|
||||
}
|
||||
.focused($focusedField, equals: Focus.new)
|
||||
Spacer()
|
||||
if !newTag.isEmpty {
|
||||
Button {
|
||||
addNewTag()
|
||||
} label: {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.tint(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var metadataSection: some View {
|
||||
Section {
|
||||
TextField("add-tag-groups.edit.title.field", text: $title, axis: .horizontal)
|
||||
.focused($focusedField, equals: Focus.title)
|
||||
.onSubmit {
|
||||
focusedField = Focus.symbol
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
HStack {
|
||||
TextField("add-tag-groups.edit.icon.field", text: $sfSymbolName, axis: .horizontal)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($focusedField, equals: Focus.symbol)
|
||||
.onSubmit {
|
||||
focusedField = Focus.new
|
||||
}
|
||||
.onChange(of: sfSymbolName) { _ in
|
||||
popupTagsPresented = true
|
||||
}
|
||||
|
||||
Image(systemName: sfSymbolName)
|
||||
}
|
||||
}
|
||||
|
||||
private func addNewTag() {
|
||||
addTag(newTag.trimmingCharacters(in: .whitespaces))
|
||||
newTag = ""
|
||||
focusedField = Focus.new
|
||||
}
|
||||
|
||||
private func addTag(_ tag: String) {
|
||||
guard !tag.isEmpty else { return }
|
||||
tags.append(tag)
|
||||
}
|
||||
|
||||
private func deleteTag(_ tag: String) {
|
||||
tags.removeAll(where: { $0 == tag })
|
||||
}
|
||||
|
||||
private func save() {
|
||||
var toSave = tags
|
||||
let main = toSave.removeFirst()
|
||||
preferences.tagGroups.append(.init(
|
||||
title: title.trimmingCharacters(in: .whitespaces),
|
||||
sfSymbolName: sfSymbolName,
|
||||
main: main,
|
||||
additional: toSave
|
||||
))
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var symbolsSuggestionView: some View {
|
||||
if focusedField == .symbol && !sfSymbolName.isEmpty {
|
||||
let filteredMatches = allSymbols
|
||||
.filter { $0.contains(sfSymbolName) }
|
||||
if !filteredMatches.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(filteredMatches, id: \.self) { symbolName in
|
||||
Button {
|
||||
sfSymbolName = symbolName
|
||||
} label: {
|
||||
Image(systemName: symbolName)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
@State private var popupTagsPresented = false
|
||||
|
||||
private var keywordsSection: some View {
|
||||
Section("add-tag-groups.edit.tags") {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
HStack {
|
||||
Text(tag)
|
||||
Spacer()
|
||||
Button {
|
||||
deleteTag(tag)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { indexes in
|
||||
if let index = indexes.first {
|
||||
let tag = tags[index]
|
||||
deleteTag(tag)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
TextField("add-tag-groups.edit.tags.add", text: $newTag, axis: .horizontal)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.onSubmit {
|
||||
addNewTag()
|
||||
}
|
||||
.focused($focusedField, equals: Focus.new)
|
||||
Spacer()
|
||||
if !newTag.isEmpty {
|
||||
Button {
|
||||
addNewTag()
|
||||
} label: {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.tint(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
||||
private func addNewTag() {
|
||||
addTag(newTag.trimmingCharacters(in: .whitespaces))
|
||||
newTag = ""
|
||||
focusedField = Focus.new
|
||||
}
|
||||
|
||||
private func addTag(_ tag: String) {
|
||||
guard !tag.isEmpty else { return }
|
||||
tags.append(tag)
|
||||
}
|
||||
|
||||
private func deleteTag(_ tag: String) {
|
||||
tags.removeAll(where: { $0 == tag })
|
||||
}
|
||||
|
||||
private func save() {
|
||||
var toSave = tags
|
||||
let main = toSave.removeFirst()
|
||||
preferences.tagGroups.append(.init(
|
||||
title: title.trimmingCharacters(in: .whitespaces),
|
||||
sfSymbolName: sfSymbolName,
|
||||
main: main,
|
||||
additional: toSave
|
||||
))
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var symbolsSuggestionView: some View {
|
||||
if focusedField == .symbol && !sfSymbolName.isEmpty {
|
||||
let filteredMatches = allSymbols
|
||||
.filter { $0.contains(sfSymbolName) }
|
||||
if !filteredMatches.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(filteredMatches, id: \.self) { symbolName in
|
||||
Button {
|
||||
sfSymbolName = symbolName
|
||||
} label: {
|
||||
Image(systemName: symbolName)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
}
|
||||
.frame(height: 40)
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct AddTagGroupView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddTagGroupView()
|
||||
.withEnvironments()
|
||||
}
|
||||
static var previews: some View {
|
||||
AddTagGroupView()
|
||||
.withEnvironments()
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
let allSymbols: [String] = {
|
||||
if let bundle = Bundle(identifier: "com.apple.CoreGlyphs"),
|
||||
let resourcePath = bundle.path(forResource: "symbol_search", ofType: "plist"),
|
||||
let plist = NSDictionary(contentsOfFile: resourcePath) {
|
||||
|
||||
return plist.allKeys as? [String] ?? []
|
||||
}
|
||||
return []
|
||||
if let bundle = Bundle(identifier: "com.apple.CoreGlyphs"),
|
||||
let resourcePath = bundle.path(forResource: "symbol_search", ofType: "plist"),
|
||||
let plist = NSDictionary(contentsOfFile: resourcePath)
|
||||
{
|
||||
return plist.allKeys as? [String] ?? []
|
||||
}
|
||||
return []
|
||||
}()
|
||||
|
@ -151,25 +151,25 @@ struct TimelineTab: View {
|
||||
Label("timeline.filter.add-local", systemImage: "badge.plus.radiowaves.right")
|
||||
}
|
||||
}
|
||||
|
||||
Menu("timeline.filter.tag-groups") {
|
||||
ForEach(preferences.tagGroups, id: \.self) { group in
|
||||
Button {
|
||||
timeline = .tagGroup(group)
|
||||
} label: {
|
||||
VStack {
|
||||
let icon = group.sfSymbolName.isEmpty ? "number" : group.sfSymbolName
|
||||
Label(group.title, systemImage: icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
routerPath.presentedSheet = .addTagGroup
|
||||
} label: {
|
||||
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||
|
||||
Menu("timeline.filter.tag-groups") {
|
||||
ForEach(preferences.tagGroups, id: \.self) { group in
|
||||
Button {
|
||||
timeline = .tagGroup(group)
|
||||
} label: {
|
||||
VStack {
|
||||
let icon = group.sfSymbolName.isEmpty ? "number" : group.sfSymbolName
|
||||
Label(group.title, systemImage: icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
routerPath.presentedSheet = .addTagGroup
|
||||
} label: {
|
||||
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var addAccountButton: some View {
|
||||
|
@ -316,7 +316,7 @@ public struct AccountDetailView: View {
|
||||
Image(systemName: "arrowshape.turn.up.left")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Menu {
|
||||
AccountDetailContextMenu(viewModel: viewModel)
|
||||
|
||||
|
@ -99,7 +99,7 @@ public struct AppAccountsSelectorView: View {
|
||||
return theme.secondaryBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var accountsView: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
|
@ -201,26 +201,25 @@ struct ConversationsListRow: View {
|
||||
// Add in each detected link in the content
|
||||
ForEach(lastStatus.content.links) { link in
|
||||
switch link.type {
|
||||
case .url:
|
||||
if UIApplication.shared.canOpenURL(link.url) {
|
||||
Button("accessibility.tabs.timeline.content-link-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = routerPath.handle(url: link.url)
|
||||
}
|
||||
}
|
||||
case .hashtag:
|
||||
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = routerPath.handle(url: link.url)
|
||||
}
|
||||
case .mention:
|
||||
Button("\(link.title)") {
|
||||
case .url:
|
||||
if UIApplication.shared.canOpenURL(link.url) {
|
||||
Button("accessibility.tabs.timeline.content-link-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = routerPath.handle(url: link.url)
|
||||
}
|
||||
}
|
||||
case .hashtag:
|
||||
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = routerPath.handle(url: link.url)
|
||||
}
|
||||
case .mention:
|
||||
Button("\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = routerPath.handle(url: link.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ public extension View {
|
||||
.accessibilityInputLabels([
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.label"),
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel1"),
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2")
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2"),
|
||||
])
|
||||
}
|
||||
}
|
||||
@ -42,7 +42,7 @@ public struct StatusEditorToolbarItem: ToolbarContent {
|
||||
.accessibilityInputLabels([
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.label"),
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel1"),
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2")
|
||||
LocalizedStringKey("accessibility.tabs.timeline.new-post.inputLabel2"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import SwiftUI
|
||||
public enum PreferredShareButtonBehavior: Int, CaseIterable, Codable {
|
||||
case linkOnly
|
||||
case linkAndText
|
||||
|
||||
|
||||
public var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .linkOnly: return "settings.content.sharing.share-behavior.link-only"
|
||||
|
@ -33,7 +33,7 @@ public enum SheetDestination: Identifiable {
|
||||
case listAddAccount(account: Account)
|
||||
case addAccount
|
||||
case addRemoteLocalTimeline
|
||||
case addTagGroup
|
||||
case addTagGroup
|
||||
case statusEditHistory(status: String)
|
||||
case settings
|
||||
case accountPushNotficationsSettings
|
||||
@ -52,7 +52,7 @@ case addTagGroup
|
||||
case .addAccount:
|
||||
return "addAccount"
|
||||
case .addTagGroup:
|
||||
return "addTagGroup"
|
||||
return "addTagGroup"
|
||||
case .addRemoteLocalTimeline:
|
||||
return "addRemoteLocalTimeline"
|
||||
case .statusEditHistory:
|
||||
|
@ -12,7 +12,7 @@ public class StreamWatcher: ObservableObject {
|
||||
|
||||
private let decoder = JSONDecoder()
|
||||
private let encoder = JSONEncoder()
|
||||
|
||||
|
||||
private var retryDelay: Int = 10
|
||||
|
||||
public enum Stream: String {
|
||||
@ -24,7 +24,6 @@ public class StreamWatcher: ObservableObject {
|
||||
@Published public var events: [any StreamEvent] = []
|
||||
@Published public var unreadNotificationsCount: Int = 0
|
||||
@Published public var latestEvent: (any StreamEvent)?
|
||||
|
||||
|
||||
public init() {
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
@ -66,7 +65,8 @@ public class StreamWatcher: ObservableObject {
|
||||
|
||||
private func sendMessage(message: StreamMessage) {
|
||||
if let encodedMessage = try? encoder.encode(message),
|
||||
let stringMessage = String(data: encodedMessage, encoding: .utf8) {
|
||||
let stringMessage = String(data: encodedMessage, encoding: .utf8)
|
||||
{
|
||||
task?.send(.string(stringMessage), completionHandler: { _ in })
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ public class UserPreferences: ObservableObject {
|
||||
private var client: Client?
|
||||
|
||||
@AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = []
|
||||
@AppStorage("tag_groups") public var tagGroups: [TagGroup] = []
|
||||
@AppStorage("tag_groups") public var tagGroups: [TagGroup] = []
|
||||
@AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari
|
||||
@AppStorage("draft_posts") public var draftsPosts: [String] = []
|
||||
@AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true
|
||||
@ -55,7 +55,7 @@ public class UserPreferences: ObservableObject {
|
||||
@AppStorage("requested_review") public var requestedReview = false
|
||||
|
||||
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
|
||||
|
||||
|
||||
@AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkAndText
|
||||
|
||||
public enum SwipeActionsIconStyle: String, CaseIterable {
|
||||
|
@ -90,7 +90,7 @@ public struct ExploreView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var quickAccessView: some View {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
|
@ -7,7 +7,7 @@ import SwiftUI
|
||||
class ExploreViewModel: ObservableObject {
|
||||
enum SearchScope: String, CaseIterable {
|
||||
case all, people, hashtags, posts
|
||||
|
||||
|
||||
var localizedString: LocalizedStringKey {
|
||||
switch self {
|
||||
case .all:
|
||||
@ -21,7 +21,7 @@ class ExploreViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var client: Client? {
|
||||
didSet {
|
||||
if oldValue != client {
|
||||
|
@ -11,7 +11,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
|
||||
public var asMarkdown: String = ""
|
||||
public var asRawText: String = ""
|
||||
public var statusesURLs = [URL]()
|
||||
private(set) public var links = [Link]()
|
||||
public private(set) var links = [Link]()
|
||||
|
||||
public var asSafeMarkdownAttributedString: AttributedString = .init()
|
||||
private var main_regex: NSRegularExpression?
|
||||
@ -156,7 +156,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
|
||||
asMarkdown += ")"
|
||||
|
||||
if let url = URL(string: href) {
|
||||
let displayString = asMarkdown[start..<finish]
|
||||
let displayString = asMarkdown[start ..< finish]
|
||||
links.append(Link(url, displayString: String(displayString)))
|
||||
}
|
||||
return
|
||||
@ -190,19 +190,19 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
|
||||
self.displayString = displayString
|
||||
|
||||
switch displayString.first {
|
||||
case "@":
|
||||
self.type = .mention
|
||||
self.title = displayString
|
||||
case "#":
|
||||
self.type = .hashtag
|
||||
self.title = String(displayString.dropFirst())
|
||||
default:
|
||||
self.type = .url
|
||||
var hostNameUrl = url.host ?? url.absoluteString
|
||||
if hostNameUrl.hasPrefix("www.") {
|
||||
hostNameUrl = String(hostNameUrl.dropFirst(4))
|
||||
}
|
||||
self.title = hostNameUrl
|
||||
case "@":
|
||||
type = .mention
|
||||
title = displayString
|
||||
case "#":
|
||||
type = .hashtag
|
||||
title = String(displayString.dropFirst())
|
||||
default:
|
||||
type = .url
|
||||
var hostNameUrl = url.host ?? url.absoluteString
|
||||
if hostNameUrl.hasPrefix("www.") {
|
||||
hostNameUrl = String(hostNameUrl.dropFirst(4))
|
||||
}
|
||||
title = hostNameUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,14 +27,14 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
|
||||
public var localizedTypeDescription: String? {
|
||||
if let supportedType {
|
||||
switch supportedType {
|
||||
case .image:
|
||||
return NSLocalizedString("accessibility.media.supported-type.image.label", bundle: .main, comment: "A localized description of SupportedType.image")
|
||||
case .gifv:
|
||||
return NSLocalizedString("accessibility.media.supported-type.gifv.label", bundle: .main, comment: "A localized description of SupportedType.gifv")
|
||||
case .video:
|
||||
return NSLocalizedString("accessibility.media.supported-type.video.label", bundle: .main, comment: "A localized description of SupportedType.video")
|
||||
case .audio:
|
||||
return NSLocalizedString("accessibility.media.supported-type.audio.label", bundle: .main, comment: "A localized description of SupportedType.audio")
|
||||
case .image:
|
||||
return NSLocalizedString("accessibility.media.supported-type.image.label", bundle: .main, comment: "A localized description of SupportedType.image")
|
||||
case .gifv:
|
||||
return NSLocalizedString("accessibility.media.supported-type.gifv.label", bundle: .main, comment: "A localized description of SupportedType.gifv")
|
||||
case .video:
|
||||
return NSLocalizedString("accessibility.media.supported-type.video.label", bundle: .main, comment: "A localized description of SupportedType.video")
|
||||
case .audio:
|
||||
return NSLocalizedString("accessibility.media.supported-type.audio.label", bundle: .main, comment: "A localized description of SupportedType.audio")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -63,28 +63,26 @@ extension Tag: Sendable {}
|
||||
extension Tag.History: Sendable {}
|
||||
extension FeaturedTag: Sendable {}
|
||||
|
||||
|
||||
public struct TagGroup: Codable, Equatable, Hashable {
|
||||
public init(title: String, sfSymbolName: String, main: String, additional: [String]) {
|
||||
self.title = title
|
||||
self.sfSymbolName = sfSymbolName
|
||||
self.main = main
|
||||
self.additional = additional
|
||||
}
|
||||
|
||||
public let title: String
|
||||
public let sfSymbolName: String
|
||||
public let main: String
|
||||
public let additional: [String]
|
||||
|
||||
public var tags: [String] {
|
||||
[main] + additional
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
tags
|
||||
.map { "#\($0)" }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
public init(title: String, sfSymbolName: String, main: String, additional: [String]) {
|
||||
self.title = title
|
||||
self.sfSymbolName = sfSymbolName
|
||||
self.main = main
|
||||
self.additional = additional
|
||||
}
|
||||
|
||||
public let title: String
|
||||
public let sfSymbolName: String
|
||||
public let main: String
|
||||
public let additional: [String]
|
||||
|
||||
public var tags: [String] {
|
||||
[main] + additional
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
tags
|
||||
.map { "#\($0)" }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ public enum Media: Endpoint {
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
public var jsonValue: Encodable? {
|
||||
switch self {
|
||||
case let .media(_, json):
|
||||
@ -25,7 +25,6 @@ public enum Media: Endpoint {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct MediaDescriptionData: Encodable, Sendable {
|
||||
@ -35,4 +34,3 @@ public struct MediaDescriptionData: Encodable, Sendable {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ public enum Timelines: Endpoint {
|
||||
case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool)
|
||||
case home(sinceId: String?, maxId: String?, minId: String?)
|
||||
case list(listId: String, sinceId: String?, maxId: String?, minId: String?)
|
||||
case hashtag(tag: String, additional: [String]?, maxId: String?)
|
||||
case hashtag(tag: String, additional: [String]?, maxId: String?)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
@ -18,7 +18,7 @@ public enum Timelines: Endpoint {
|
||||
return "timelines/tag/\(tag)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
switch self {
|
||||
case let .pub(sinceId, maxId, minId, local):
|
||||
@ -30,10 +30,10 @@ public enum Timelines: Endpoint {
|
||||
case let .list(_, sinceId, maxId, mindId):
|
||||
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||
case let .hashtag(_, additional, maxId):
|
||||
var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) ?? []
|
||||
params.append(contentsOf: (additional ?? [])
|
||||
.map { URLQueryItem(name: "any[]", value: $0) })
|
||||
return params
|
||||
var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) ?? []
|
||||
params.append(contentsOf: (additional ?? [])
|
||||
.map { URLQueryItem(name: "any[]", value: $0) })
|
||||
return params
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ public struct StatusDetailView: View {
|
||||
@EnvironmentObject private var watcher: StreamWatcher
|
||||
@EnvironmentObject private var client: Client
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
|
||||
|
||||
@StateObject private var viewModel: StatusDetailViewModel
|
||||
|
||||
|
||||
@State private var isLoaded: Bool = false
|
||||
@State private var statusHeight: CGFloat = 0
|
||||
|
||||
|
@ -1,35 +1,33 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct StatusEditorCameraPickerView: UIViewControllerRepresentable {
|
||||
@Binding var selectedImage: UIImage?
|
||||
@Environment(\.presentationMode) var isPresented
|
||||
|
||||
|
||||
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
|
||||
let picker: StatusEditorCameraPickerView
|
||||
|
||||
|
||||
init(picker: StatusEditorCameraPickerView) {
|
||||
self.picker = picker
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
|
||||
func imagePickerController(_: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
guard let selectedImage = info[.originalImage] as? UIImage else { return }
|
||||
self.picker.selectedImage = selectedImage
|
||||
self.picker.isPresented.wrappedValue.dismiss()
|
||||
picker.selectedImage = selectedImage
|
||||
picker.isPresented.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
let imagePicker = UIImagePickerController()
|
||||
imagePicker.sourceType = .camera
|
||||
imagePicker.delegate = context.coordinator
|
||||
return imagePicker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
func updateUIViewController(_: UIImagePickerController, context _: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(picker: self)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ struct StatusEditorMediaEditView: View {
|
||||
@FocusState private var isFieldFocused: Bool
|
||||
|
||||
@State private var isUpdating: Bool = false
|
||||
|
||||
|
||||
@State private var didAppear: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
@ -71,7 +71,8 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||
if self == .jpeg || self == .png || self == .tiff || self == .image || self == .uiimage || self == .adobeRawImage {
|
||||
if let image = result as? UIImage,
|
||||
let compressedData = try? await compressor.compressImageForUpload(image),
|
||||
let compressedImage = UIImage(data: compressedData) {
|
||||
let compressedImage = UIImage(data: compressedData)
|
||||
{
|
||||
return compressedImage
|
||||
} else if let imageURL = result as? URL,
|
||||
let compressedData = await compressor.compressImageFrom(url: imageURL),
|
||||
|
@ -368,7 +368,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject {
|
||||
.compactMap { NSItemProvider(contentsOf: $0) }
|
||||
processItemsProvider(items: items)
|
||||
}
|
||||
|
||||
|
||||
func processCameraPhoto(image: UIImage) {
|
||||
mediasImages.append(.init(image: image,
|
||||
movieTransferable: nil,
|
||||
|
@ -75,7 +75,7 @@ public class ReblogCache {
|
||||
if let reblog = status.reblog {
|
||||
if let cached = statusCache.value(forKey: reblog.id) {
|
||||
// this is already cached
|
||||
if cached.postId != status.id && cached.seen {
|
||||
if cached.postId != status.id, cached.seen {
|
||||
// This was posted by someone other than the person we have in the cache
|
||||
// and we have seen the items at some point, so we might want to suppress it
|
||||
|
||||
|
@ -117,7 +117,6 @@ public struct StatusPollView: View {
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel(viewModel.poll.expired ? "accessibility.status.poll.finished.label" : "accessibility.status.poll.active.label")
|
||||
|
||||
}
|
||||
|
||||
func combinedAccessibilityLabel(for option: Poll.Option, index: Int) -> Text {
|
||||
@ -126,7 +125,6 @@ public struct StatusPollView: View {
|
||||
Text(", ") +
|
||||
Text(option.title) +
|
||||
Text(showPercentage ? ", \(percentForOption(option: option))%" : "")
|
||||
|
||||
}
|
||||
|
||||
private var footerView: some View {
|
||||
|
@ -124,7 +124,7 @@ public struct StatusRowView: View {
|
||||
trailing: .layoutPadding))
|
||||
.accessibilityElement(children: viewModel.isFocused ? .contain : .combine)
|
||||
.accessibilityLabel(viewModel.isFocused == false && accessibilityEnabled
|
||||
? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text(""))
|
||||
? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text(""))
|
||||
.accessibilityHidden(viewModel.filter?.filter.filterAction == .hide)
|
||||
.accessibilityAction {
|
||||
viewModel.navigateToDetail()
|
||||
@ -214,23 +214,23 @@ public struct StatusRowView: View {
|
||||
// Add in each detected link in the content
|
||||
ForEach(viewModel.finalStatus.content.links) { link in
|
||||
switch link.type {
|
||||
case .url:
|
||||
if UIApplication.shared.canOpenURL(link.url) {
|
||||
Button("accessibility.tabs.timeline.content-link-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = viewModel.routerPath.handle(url: link.url)
|
||||
}
|
||||
}
|
||||
case .hashtag:
|
||||
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = viewModel.routerPath.handle(url: link.url)
|
||||
}
|
||||
case .mention:
|
||||
Button("\(link.title)") {
|
||||
case .url:
|
||||
if UIApplication.shared.canOpenURL(link.url) {
|
||||
Button("accessibility.tabs.timeline.content-link-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = viewModel.routerPath.handle(url: link.url)
|
||||
}
|
||||
}
|
||||
case .hashtag:
|
||||
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = viewModel.routerPath.handle(url: link.url)
|
||||
}
|
||||
case .mention:
|
||||
Button("\(link.title)") {
|
||||
HapticManager.shared.fireHaptic(of: .notification(.success))
|
||||
_ = viewModel.routerPath.handle(url: link.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -298,28 +298,27 @@ private struct CombinedAccessibilityLabel {
|
||||
func finalLabel() -> Text {
|
||||
if let filter {
|
||||
switch filter.filterAction {
|
||||
case .warn:
|
||||
return Text("status.filter.filtered-by-\(filter.title)")
|
||||
case .hide:
|
||||
return Text("")
|
||||
case .warn:
|
||||
return Text("status.filter.filtered-by-\(filter.title)")
|
||||
case .hide:
|
||||
return Text("")
|
||||
}
|
||||
} else {
|
||||
return userNamePreamble() +
|
||||
Text(hasSpoiler
|
||||
? viewModel.finalStatus.spoilerText.asRawText
|
||||
: viewModel.finalStatus.content.asRawText
|
||||
) +
|
||||
Text(hasSpoiler
|
||||
? "status.editor.spoiler"
|
||||
: ""
|
||||
) + Text(", ") +
|
||||
pollText() +
|
||||
imageAltText() +
|
||||
Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") +
|
||||
Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") +
|
||||
Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") +
|
||||
Text("status.summary.n-favorites \(viewModel.finalStatus.favouritesCount)")
|
||||
|
||||
Text(hasSpoiler
|
||||
? viewModel.finalStatus.spoilerText.asRawText
|
||||
: viewModel.finalStatus.content.asRawText
|
||||
) +
|
||||
Text(hasSpoiler
|
||||
? "status.editor.spoiler"
|
||||
: ""
|
||||
) + Text(", ") +
|
||||
pollText() +
|
||||
imageAltText() +
|
||||
Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") +
|
||||
Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") +
|
||||
Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") +
|
||||
Text("status.summary.n-favorites \(viewModel.finalStatus.favouritesCount)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,12 +376,12 @@ private struct CombinedAccessibilityLabel {
|
||||
: 0
|
||||
|
||||
text = text +
|
||||
Text(selected ? "accessibility.status.poll.selected.label" : "") +
|
||||
Text(", ") +
|
||||
Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") +
|
||||
Text(", ") +
|
||||
Text(option.title) +
|
||||
Text(showPercentage ? ", \(percentage)%. " : ". ")
|
||||
Text(selected ? "accessibility.status.poll.selected.label" : "") +
|
||||
Text(", ") +
|
||||
Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") +
|
||||
Text(", ") +
|
||||
Text(option.title) +
|
||||
Text(showPercentage ? ", \(percentage)%. " : ". ")
|
||||
}
|
||||
}
|
||||
return Text("")
|
||||
|
@ -33,13 +33,14 @@ public class StatusRowViewModel: ObservableObject {
|
||||
@Published var isLoadingRemoteContent: Bool = false
|
||||
@Published var localStatusId: String?
|
||||
@Published var localStatus: Status?
|
||||
|
||||
|
||||
// The relationship our user has to the author of this post, if available
|
||||
@Published var authorRelationship: Relationship? {
|
||||
didSet {
|
||||
// if we are newly blocking or muting the author, force collapse post so it goes away
|
||||
if let relationship = authorRelationship,
|
||||
relationship.blocking || relationship.muting {
|
||||
relationship.blocking || relationship.muting
|
||||
{
|
||||
lineLimit = 0
|
||||
}
|
||||
}
|
||||
|
@ -124,8 +124,7 @@ struct StatusRowActionsView: View {
|
||||
{
|
||||
switch userPreferences.shareButtonBehavior {
|
||||
case .linkOnly:
|
||||
ShareLink(item: url)
|
||||
{
|
||||
ShareLink(item: url) {
|
||||
action.image(dataController: statusDataController)
|
||||
}
|
||||
.buttonStyle(.statusAction())
|
||||
|
@ -187,8 +187,7 @@ struct StatusRowContextMenu: View {
|
||||
} label: {
|
||||
Label("status.action.message", systemImage: "tray.full")
|
||||
}
|
||||
|
||||
|
||||
|
||||
if viewModel.authorRelationship?.blocking == true {
|
||||
Button {
|
||||
Task {
|
||||
@ -216,7 +215,7 @@ struct StatusRowContextMenu: View {
|
||||
Label("account.action.block", systemImage: "person.crop.circle.badge.xmark")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if viewModel.authorRelationship?.muting == true {
|
||||
Button {
|
||||
Task {
|
||||
@ -248,7 +247,6 @@ struct StatusRowContextMenu: View {
|
||||
Label("account.action.mute", systemImage: "speaker.slash")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Section {
|
||||
|
@ -31,8 +31,8 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable {
|
||||
|
||||
public enum TimelineFilter: Hashable, Equatable {
|
||||
case home, local, federated, trending
|
||||
case hashtag(tag: String, accountId: String?)
|
||||
case tagGroup(TagGroup)
|
||||
case hashtag(tag: String, accountId: String?)
|
||||
case tagGroup(TagGroup)
|
||||
case list(list: Models.List)
|
||||
case remoteLocal(server: String, filter: RemoteTimelineFilter)
|
||||
case latest
|
||||
@ -74,7 +74,7 @@ public enum TimelineFilter: Hashable, Equatable {
|
||||
case let .hashtag(tag, _):
|
||||
return "#\(tag)"
|
||||
case let .tagGroup(group):
|
||||
return group.title
|
||||
return group.title
|
||||
case let .list(list):
|
||||
return list.title
|
||||
case let .remoteLocal(server, _):
|
||||
@ -97,7 +97,7 @@ public enum TimelineFilter: Hashable, Equatable {
|
||||
case let .hashtag(tag, _):
|
||||
return "#\(tag)"
|
||||
case let .tagGroup(group):
|
||||
return LocalizedStringKey(group.title) // ?? not sure since this can't be localized.
|
||||
return LocalizedStringKey(group.title) // ?? not sure since this can't be localized.
|
||||
case let .list(list):
|
||||
return LocalizedStringKey(list.title)
|
||||
case let .remoteLocal(server, _):
|
||||
@ -147,10 +147,10 @@ public enum TimelineFilter: Hashable, Equatable {
|
||||
if let accountId {
|
||||
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil)
|
||||
} else {
|
||||
return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId)
|
||||
return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId)
|
||||
}
|
||||
case let .tagGroup(group):
|
||||
return Timelines.hashtag(tag: group.main, additional: group.additional, maxId: maxId)
|
||||
return Timelines.hashtag(tag: group.main, additional: group.additional, maxId: maxId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,7 +162,7 @@ extension TimelineFilter: Codable {
|
||||
case federated
|
||||
case trending
|
||||
case hashtag
|
||||
case tagGroup
|
||||
case tagGroup
|
||||
case list
|
||||
case remoteLocal
|
||||
case latest
|
||||
@ -189,8 +189,8 @@ extension TimelineFilter: Codable {
|
||||
accountId: accountId
|
||||
)
|
||||
case .tagGroup:
|
||||
let group = try container.decode(TagGroup.self, forKey: .tagGroup)
|
||||
self = .tagGroup(group)
|
||||
let group = try container.decode(TagGroup.self, forKey: .tagGroup)
|
||||
self = .tagGroup(group)
|
||||
case .list:
|
||||
let list = try container.decode(
|
||||
Models.List.self,
|
||||
@ -233,7 +233,7 @@ extension TimelineFilter: Codable {
|
||||
try nestedContainer.encode(tag)
|
||||
try nestedContainer.encode(accountId)
|
||||
case let .tagGroup(group):
|
||||
try container.encode(group, forKey: .tagGroup)
|
||||
try container.encode(group, forKey: .tagGroup)
|
||||
case let .list(list):
|
||||
try container.encode(list, forKey: .list)
|
||||
case let .remoteLocal(server, filter):
|
||||
|
@ -1,11 +1,11 @@
|
||||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUIIntrospect
|
||||
import Models
|
||||
import Network
|
||||
import Shimmer
|
||||
import Status
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
public struct TimelineView: View {
|
||||
private enum Constants {
|
||||
@ -39,8 +39,8 @@ public struct TimelineView: View {
|
||||
ScrollViewReader { proxy in
|
||||
ZStack(alignment: .top) {
|
||||
List {
|
||||
if viewModel.tagGroup != nil {
|
||||
tagGroupHeaderView
|
||||
if viewModel.tagGroup != nil {
|
||||
tagGroupHeaderView
|
||||
} else if viewModel.tag == nil {
|
||||
scrollToTopView
|
||||
} else {
|
||||
@ -106,19 +106,18 @@ public struct TimelineView: View {
|
||||
}
|
||||
.accessibilityRepresentation {
|
||||
switch timeline {
|
||||
case let .remoteLocal(_, filter):
|
||||
if canFilterTimeline {
|
||||
Menu(filter.localizedTitle()) {}
|
||||
} else {
|
||||
Text(filter.localizedTitle())
|
||||
}
|
||||
default:
|
||||
if canFilterTimeline {
|
||||
Menu(timeline.localizedTitle()) {}
|
||||
} else {
|
||||
Text(timeline.localizedTitle())
|
||||
}
|
||||
|
||||
case let .remoteLocal(_, filter):
|
||||
if canFilterTimeline {
|
||||
Menu(filter.localizedTitle()) {}
|
||||
} else {
|
||||
Text(filter.localizedTitle())
|
||||
}
|
||||
default:
|
||||
if canFilterTimeline {
|
||||
Menu(timeline.localizedTitle()) {}
|
||||
} else {
|
||||
Text(timeline.localizedTitle())
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityAddTraits(.isHeader)
|
||||
@ -182,64 +181,64 @@ public struct TimelineView: View {
|
||||
@ViewBuilder
|
||||
private var tagHeaderView: some View {
|
||||
if let tag = viewModel.tag {
|
||||
headerView {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("#\(tag.name)")
|
||||
.font(.scaledHeadline)
|
||||
Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)")
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if tag.following {
|
||||
viewModel.tag = await account.unfollowTag(id: tag.name)
|
||||
} else {
|
||||
viewModel.tag = await account.followTag(id: tag.name)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(tag.following ? "account.follow.following" : "account.follow.follow")
|
||||
}.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tagGroupHeaderView: some View {
|
||||
if let group = viewModel.tagGroup {
|
||||
headerView {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(group.description)
|
||||
.font(.scaledHeadline)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
headerView {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("#\(tag.name)")
|
||||
.font(.scaledHeadline)
|
||||
Text("timeline.n-recent-from-n-participants \(tag.totalUses) \(tag.totalAccounts)")
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if tag.following {
|
||||
viewModel.tag = await account.unfollowTag(id: tag.name)
|
||||
} else {
|
||||
viewModel.tag = await account.followTag(id: tag.name)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(tag.following ? "account.follow.following" : "account.follow.follow")
|
||||
}.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func headerView(
|
||||
@ViewBuilder content: () -> some View
|
||||
) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
content()
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tagGroupHeaderView: some View {
|
||||
if let group = viewModel.tagGroup {
|
||||
headerView {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(group.description)
|
||||
.font(.scaledHeadline)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.init(top: 8,
|
||||
leading: .layoutPadding,
|
||||
bottom: 8,
|
||||
trailing: .layoutPadding))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func headerView(
|
||||
@ViewBuilder content: () -> some View
|
||||
) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
content()
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.init(top: 8,
|
||||
leading: .layoutPadding,
|
||||
bottom: 8,
|
||||
trailing: .layoutPadding))
|
||||
}
|
||||
|
||||
private var scrollToTopView: some View {
|
||||
HStack { EmptyView() }
|
||||
|
@ -40,13 +40,13 @@ class TimelineViewModel: ObservableObject {
|
||||
private var timelineTask: Task<Void, Never>?
|
||||
|
||||
@Published var tag: Tag?
|
||||
|
||||
var tagGroup: TagGroup? {
|
||||
if case let .tagGroup(group) = timeline {
|
||||
return group
|
||||
}
|
||||
return nil
|
||||
|
||||
var tagGroup: TagGroup? {
|
||||
if case let .tagGroup(group) = timeline {
|
||||
return group
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Internal source of truth for a timeline.
|
||||
private var datasource = TimelineDatasource()
|
||||
@ -170,7 +170,7 @@ extension TimelineViewModel: StatusesFetcher {
|
||||
}
|
||||
await fetchNewestStatuses()
|
||||
}
|
||||
|
||||
|
||||
func refreshTimeline() {
|
||||
timelineTask?.cancel()
|
||||
timelineTask = Task {
|
||||
@ -321,8 +321,9 @@ extension TimelineViewModel: StatusesFetcher {
|
||||
// If none, it'll stop there.
|
||||
// Only do that in the context of the home timeline as other don't worth catching up that much.
|
||||
if timeline == .home,
|
||||
!Task.isCancelled,
|
||||
let latest = await datasource.get().first {
|
||||
!Task.isCancelled,
|
||||
let latest = await datasource.get().first
|
||||
{
|
||||
try await fetchNewPagesFrom(latestStatus: latest, client: client)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user