Support filters in statuses
This commit is contained in:
parent
37a5567fe7
commit
f4f8b81f6c
|
@ -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
|
||||
}
|
|
@ -38,6 +38,7 @@ public protocol AnyStatus {
|
|||
var visibility: Visibility { get }
|
||||
var poll: Poll? { 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 poll: Poll?
|
||||
public let spoilerText: String
|
||||
public let filtered: [Filtered]
|
||||
|
||||
public static func placeholder() -> Status {
|
||||
.init(id: UUID().uuidString,
|
||||
|
@ -91,7 +93,8 @@ public struct Status: AnyStatus, Codable, Identifiable {
|
|||
inReplyToAccountId: nil,
|
||||
visibility: .pub,
|
||||
poll: nil,
|
||||
spoilerText: "")
|
||||
spoilerText: "",
|
||||
filtered: [])
|
||||
}
|
||||
|
||||
public static func placeholders() -> [Status] {
|
||||
|
@ -125,4 +128,5 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable {
|
|||
public let visibility: Visibility
|
||||
public let poll: Poll?
|
||||
public let spoilerText: String
|
||||
public let filtered: [Filtered]
|
||||
}
|
||||
|
|
|
@ -18,44 +18,66 @@ public struct StatusRowView: View {
|
|||
}
|
||||
|
||||
public var body: some View {
|
||||
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)
|
||||
if viewModel.isFiltered, let filter = viewModel.filter {
|
||||
switch filter.filter.filterAction {
|
||||
case .warn:
|
||||
makeFilterView(filter: filter.filter)
|
||||
case .hide:
|
||||
EmptyView()
|
||||
}
|
||||
} 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) {
|
||||
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))
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.client = client
|
||||
if !viewModel.isCompact, viewModel.embededStatus == nil {
|
||||
Task {
|
||||
await viewModel.loadEmbededStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
contextMenu
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.client = client
|
||||
if !viewModel.isCompact, viewModel.embededStatus == nil {
|
||||
Task {
|
||||
await viewModel.loadEmbededStatus()
|
||||
}
|
||||
|
||||
private func makeFilterView(filter: Filter) -> some View {
|
||||
HStack {
|
||||
Text("Filtered by: \(filter.title)")
|
||||
Button {
|
||||
withAnimation {
|
||||
viewModel.isFiltered = false
|
||||
}
|
||||
} label: {
|
||||
Text("Show anyway")
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
contextMenu
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -16,6 +16,11 @@ public class StatusRowViewModel: ObservableObject {
|
|||
@Published var embededStatus: Status?
|
||||
@Published var displaySpoiler: Bool = false
|
||||
@Published var isEmbedLoading: Bool = true
|
||||
@Published var isFiltered: Bool = false
|
||||
|
||||
var filter: Filtered? {
|
||||
status.reblog?.filtered.first ?? status.filtered.first
|
||||
}
|
||||
|
||||
var client: Client?
|
||||
|
||||
|
@ -36,6 +41,8 @@ public class StatusRowViewModel: ObservableObject {
|
|||
self.reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount
|
||||
self.repliesCount = status.reblog?.repliesCount ?? status.repliesCount
|
||||
self.displaySpoiler = !status.spoilerText.isEmpty
|
||||
|
||||
self.isFiltered = filter != nil
|
||||
}
|
||||
|
||||
func loadEmbededStatus() async {
|
||||
|
|
|
@ -31,7 +31,9 @@ For contributors and myself, here is a todo list of features that could be added
|
|||
- [ ] Handle emoji in status
|
||||
- [X] Light theme
|
||||
- [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
|
||||
- [ ] More context menu everywhere
|
||||
- [ ] Support pinned posts
|
||||
|
|
Loading…
Reference in New Issue