Support filters in statuses

This commit is contained in:
Thomas Ricouard 2023-01-03 12:24:15 +01:00
parent 37a5567fe7
commit f4f8b81f6c
5 changed files with 89 additions and 32 deletions

View file

@ -0,0 +1,22 @@
import Foundation
public struct Filtered: Codable {
public let filter: Filter
public let keywordMatches: [String]?
}
public struct Filter: Codable, Identifiable {
public enum Action: String, Codable {
case warn, hide
}
public enum Context: String, Codable {
case home, notifications, account, thread
case pub = "public"
}
public let id: String
public let title: String
public let context: [String]
public let filterAction: Action
}

View file

@ -38,6 +38,7 @@ public protocol AnyStatus {
var visibility: Visibility { get } var visibility: Visibility { get }
var poll: Poll? { get } var poll: Poll? { get }
var spoilerText: String { get } var spoilerText: String { get }
var filtered: [Filtered] { get }
} }
@ -68,6 +69,7 @@ public struct Status: AnyStatus, Codable, Identifiable {
public let visibility: Visibility public let visibility: Visibility
public let poll: Poll? public let poll: Poll?
public let spoilerText: String public let spoilerText: String
public let filtered: [Filtered]
public static func placeholder() -> Status { public static func placeholder() -> Status {
.init(id: UUID().uuidString, .init(id: UUID().uuidString,
@ -91,7 +93,8 @@ public struct Status: AnyStatus, Codable, Identifiable {
inReplyToAccountId: nil, inReplyToAccountId: nil,
visibility: .pub, visibility: .pub,
poll: nil, poll: nil,
spoilerText: "") spoilerText: "",
filtered: [])
} }
public static func placeholders() -> [Status] { public static func placeholders() -> [Status] {
@ -125,4 +128,5 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable {
public let visibility: Visibility public let visibility: Visibility
public let poll: Poll? public let poll: Poll?
public let spoilerText: String public let spoilerText: String
public let filtered: [Filtered]
} }

View file

@ -18,44 +18,66 @@ public struct StatusRowView: View {
} }
public var body: some View { public var body: some View {
HStack(alignment: .top, spacing: .statusColumnsSpacing) { if viewModel.isFiltered, let filter = viewModel.filter {
if !viewModel.isCompact, switch filter.filter.filterAction {
theme.avatarPosition == .leading, case .warn:
let status: AnyStatus = viewModel.status.reblog ?? viewModel.status { makeFilterView(filter: filter.filter)
Button { case .hide:
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) EmptyView()
} label: { }
AvatarView(url: status.account.avatar, size: .status) } else {
HStack(alignment: .top, spacing: .statusColumnsSpacing) {
if !viewModel.isCompact,
theme.avatarPosition == .leading,
let status: AnyStatus = viewModel.status.reblog ?? viewModel.status {
Button {
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
} label: {
AvatarView(url: status.account.avatar, size: .status)
}
}
VStack(alignment: .leading) {
if !viewModel.isCompact {
reblogView
replyView
}
statusView
if !viewModel.isCompact {
StatusActionsView(viewModel: viewModel)
.padding(.vertical, 8)
.tint(viewModel.isFocused ? theme.tintColor : .gray)
.contentShape(Rectangle())
.onTapGesture {
routeurPath.navigate(to: .statusDetail(id: viewModel.status.reblog?.id ?? viewModel.status.id))
}
}
} }
} }
VStack(alignment: .leading) { .onAppear {
if !viewModel.isCompact { viewModel.client = client
reblogView if !viewModel.isCompact, viewModel.embededStatus == nil {
replyView Task {
} await viewModel.loadEmbededStatus()
statusView }
if !viewModel.isCompact {
StatusActionsView(viewModel: viewModel)
.padding(.vertical, 8)
.tint(viewModel.isFocused ? theme.tintColor : .gray)
.contentShape(Rectangle())
.onTapGesture {
routeurPath.navigate(to: .statusDetail(id: viewModel.status.reblog?.id ?? viewModel.status.id))
}
} }
} }
.contextMenu {
contextMenu
}
} }
.onAppear { }
viewModel.client = client
if !viewModel.isCompact, viewModel.embededStatus == nil { private func makeFilterView(filter: Filter) -> some View {
Task { HStack {
await viewModel.loadEmbededStatus() Text("Filtered by: \(filter.title)")
Button {
withAnimation {
viewModel.isFiltered = false
} }
} label: {
Text("Show anyway")
} }
} }
.contextMenu {
contextMenu
}
} }
@ViewBuilder @ViewBuilder

View file

@ -16,6 +16,11 @@ public class StatusRowViewModel: ObservableObject {
@Published var embededStatus: Status? @Published var embededStatus: Status?
@Published var displaySpoiler: Bool = false @Published var displaySpoiler: Bool = false
@Published var isEmbedLoading: Bool = true @Published var isEmbedLoading: Bool = true
@Published var isFiltered: Bool = false
var filter: Filtered? {
status.reblog?.filtered.first ?? status.filtered.first
}
var client: Client? var client: Client?
@ -36,6 +41,8 @@ public class StatusRowViewModel: ObservableObject {
self.reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount self.reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount
self.repliesCount = status.reblog?.repliesCount ?? status.repliesCount self.repliesCount = status.reblog?.repliesCount ?? status.repliesCount
self.displaySpoiler = !status.spoilerText.isEmpty self.displaySpoiler = !status.spoilerText.isEmpty
self.isFiltered = filter != nil
} }
func loadEmbededStatus() async { func loadEmbededStatus() async {

View file

@ -31,7 +31,9 @@ For contributors and myself, here is a todo list of features that could be added
- [ ] Handle emoji in status - [ ] Handle emoji in status
- [X] Light theme - [X] Light theme
- [X] More themes - [X] More themes
- [ ] Honor & display server side features (filter, default visibility, etc...) - [ ] Display & Edit server side features (filter, default visibility, etc...)
- [X] Honor filters for statuses.
- [ ] Edit filters.
- [X] Open remote status locally - [X] Open remote status locally
- [ ] More context menu everywhere - [ ] More context menu everywhere
- [ ] Support pinned posts - [ ] Support pinned posts