Compare commits

..

No commits in common. "dc83172aeae62c9bb6630a23a0d4ff7b9309b28a" and "fc7e7f502be7c61dc02a584597dee86ea62aa026" have entirely different histories.

9 changed files with 47 additions and 143 deletions

View File

@ -121,7 +121,7 @@ public class InstanceFeatures: ObservableObject {
return true return true
case .pleroma(.vanilla(let v)) where v >= Version(2, 5, 0): case .pleroma(.vanilla(let v)) where v >= Version(2, 5, 0):
return true return true
case .pleroma(.akkoma(_)): case .pleroma(.akkoma(nil)):
return true return true
default: default:
return false return false

View File

@ -22,7 +22,7 @@ struct EditStatusParameters: Encodable, Sendable {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id) try container.encode(self.id, forKey: .id)
try container.encode(self.text, forKey: .text) try container.encode(self.text, forKey: .text)
try container.encode(self.contentType.mimeType, forKey: .contentType) try container.encode(self.contentType, forKey: .contentType)
try container.encodeIfPresent(self.spoilerText, forKey: .spoilerText) try container.encodeIfPresent(self.spoilerText, forKey: .spoilerText)
try container.encode(self.sensitive, forKey: .sensitive) try container.encode(self.sensitive, forKey: .sensitive)
try container.encodeIfPresent(self.language, forKey: .language) try container.encodeIfPresent(self.language, forKey: .language)

View File

@ -12,12 +12,20 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
public private(set) var notifications: [Notification] public private(set) var notifications: [Notification]
public let id: String public let id: String
public let kind: Notification.Kind public let kind: Notification.Kind
public let statusState: CollapseState?
@MainActor
public init?(notifications: [Notification]) { public init?(notifications: [Notification]) {
guard !notifications.isEmpty else { return nil } guard !notifications.isEmpty else { return nil }
self.notifications = notifications self.notifications = notifications
self.id = notifications.first!.id self.id = notifications.first!.id
self.kind = notifications.first!.kind self.kind = notifications.first!.kind
switch kind {
case .mention, .status:
self.statusState = .unknown
default:
self.statusState = nil
}
} }
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool { public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {

View File

@ -54,12 +54,9 @@ class ExpandThreadCollectionViewCell: UICollectionViewListCell {
threadLinkViewFullHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) threadLinkViewFullHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
threadLinkViewShortHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2) threadLinkViewShortHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2)
let avatarContainerHeightConstraint = avatarContainerView.heightAnchor.constraint(equalToConstant: 32)
// let this be broken during intermediate layouts when the collection view imposes a height constraint
avatarContainerHeightConstraint.priority = .init(999)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
avatarContainerWidthConstraint, avatarContainerWidthConstraint,
avatarContainerHeightConstraint, avatarContainerView.heightAnchor.constraint(equalToConstant: 32),
stackViewLeadingConstraint, stackViewLeadingConstraint,
hStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), hStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),

View File

@ -60,7 +60,7 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
$0.adjustsFontForContentSizeCategory = true $0.adjustsFontForContentSizeCategory = true
$0.numberOfLines = 2 $0.numberOfLines = 2
$0.lineBreakMode = .byTruncatingTail $0.lineBreakMode = .byTruncatingTail
$0.combiner = { [weak self] in self?.updateActionLabel(names: $0) ?? NSAttributedString() } $0.combiner = { [unowned self] in self.updateActionLabel(names: $0) }
} }
private let statusContentLabel = UILabel().configure { private let statusContentLabel = UILabel().configure {

View File

@ -14,7 +14,6 @@ import Sentry
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController { class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private let filterer: Filterer
private let allowedTypes: [Pachyderm.Notification.Kind] private let allowedTypes: [Pachyderm.Notification.Kind]
private let groupTypes = [Pachyderm.Notification.Kind.favourite, .reblog, .follow] private let groupTypes = [Pachyderm.Notification.Kind.favourite, .reblog, .follow]
@ -32,11 +31,6 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
self.allowedTypes = allowedTypes self.allowedTypes = allowedTypes
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.controller = TimelineLikeController(delegate: self, ownerType: String(describing: self)) self.controller = TimelineLikeController(delegate: self, ownerType: String(describing: self))
@ -79,10 +73,6 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
if item.hidesSeparators { if item.hidesSeparators {
config.topSeparatorVisibility = .hidden config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden config.bottomSeparatorVisibility = .hidden
} else if case .group(_, _, .some(let filterState)) = item,
self.filterer.isKnownHide(state: filterState) {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
} else { } else {
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
@ -116,18 +106,14 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl = UIRefreshControl()
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif #endif
filterer.filtersChanged = { [unowned self] actionsChanged in
self.reapplyFilters(actionsChanged: actionsChanged)
}
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (NotificationGroup, CollapseState, Filterer.Result, NSAttributedString?)> { [unowned self] cell, indexPath, itemIdentifier in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, NotificationGroup> { [unowned self] cell, indexPath, itemIdentifier in
cell.delegate = self cell.delegate = self
let statusID = itemIdentifier.0.notifications.first!.status!.id let statusID = itemIdentifier.notifications.first!.status!.id
let statusState = itemIdentifier.1 let statusState = itemIdentifier.statusState!
cell.updateUI(statusID: statusID, state: statusState, filterResult: itemIdentifier.2, precomputedContent: itemIdentifier.3) cell.updateUI(statusID: statusID, state: statusState, filterResult: .allow, precomputedContent: nil)
} }
let actionGroupCell = UICollectionView.CellRegistration<ActionNotificationGroupCollectionViewCell, NotificationGroup> { [unowned self] cell, indexPath, itemIdentifier in let actionGroupCell = UICollectionView.CellRegistration<ActionNotificationGroupCollectionViewCell, NotificationGroup> { [unowned self] cell, indexPath, itemIdentifier in
cell.delegate = self cell.delegate = self
@ -154,23 +140,12 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
config.text = "Unknown Notification" config.text = "Unknown Notification"
cell.contentConfiguration = config cell.contentConfiguration = config
} }
let zeroHeightCell = UICollectionView.CellRegistration<ZeroHeightCollectionViewCell, Void> { _, _, _ in
}
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
switch itemIdentifier { switch itemIdentifier {
case .group(let group, let collapseState, let filterState): case .group(let group):
switch group.kind { switch group.kind {
case .status, .mention: case .status, .mention:
let (result, precomputedContent) = self.filterer.resolve(state: filterState!) { return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: group)
let id = group.notifications.first!.status!.id
return (self.mastodonController.persistentContainer.status(for: id)!, false)
}
switch result {
case .allow, .warn(_):
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (group, collapseState!, result, precomputedContent))
case .hide:
return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ())
}
case .favourite, .reblog: case .favourite, .reblog:
return collectionView.dequeueConfiguredReusableCell(using: actionGroupCell, for: indexPath, item: group) return collectionView.dequeueConfiguredReusableCell(using: actionGroupCell, for: indexPath, item: group)
case .follow: case .follow:
@ -217,44 +192,8 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
} }
} }
private func filterResult(state: FilterState, statusID: String) -> (Filterer.Result, NSAttributedString?) {
let status = {
let status = self.mastodonController.persistentContainer.status(for: statusID)!
// if the status is a reblog of another one, filter based on that one
if let reblogged = status.reblog {
return (reblogged, true)
} else {
return (status, false)
}
}
return filterer.resolve(state: state, status: status)
}
private func reapplyFilters(actionsChanged: Bool) {
let visible = collectionView.indexPathsForVisibleItems
let items = visible
.compactMap { dataSource.itemIdentifier(for: $0) }
.filter {
if case .group(_, _, .some(_)) = $0 {
return true
} else {
return false
}
}
guard !items.isEmpty else {
return
}
var snapshot = dataSource.snapshot()
if actionsChanged {
snapshot.reloadItems(items)
} else {
snapshot.reconfigureItems(items)
}
dataSource.apply(snapshot)
}
private func dismissNotificationsInGroup(at indexPath: IndexPath) async { private func dismissNotificationsInGroup(at indexPath: IndexPath) async {
guard case .group(let group, let collapseState, let filterState) = dataSource.itemIdentifier(for: indexPath) else { guard case .group(let group) = dataSource.itemIdentifier(for: indexPath) else {
return return
} }
let notifications = group.notifications let notifications = group.notifications
@ -277,11 +216,11 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
} }
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
if dismissFailedIndices.isEmpty { if dismissFailedIndices.isEmpty {
snapshot.deleteItems([.group(group, collapseState, filterState)]) snapshot.deleteItems([.group(group)])
} else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count { } else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count {
let dismissFailed = dismissFailedIndices.sorted().map { notifications[$0] } let dismissFailed = dismissFailedIndices.sorted().map { notifications[$0] }
snapshot.insertItems([.group(NotificationGroup(notifications: dismissFailed)!, collapseState, filterState)], afterItem: .group(group, collapseState, filterState)) snapshot.insertItems([.group(NotificationGroup(notifications: dismissFailed)!)], afterItem: .group(group))
snapshot.deleteItems([.group(group, collapseState, filterState)]) snapshot.deleteItems([.group(group)])
} }
await apply(snapshot, animatingDifferences: true) await apply(snapshot, animatingDifferences: true)
} }
@ -296,46 +235,16 @@ extension NotificationsCollectionViewController {
static var entries: Self { .notifications } static var entries: Self { .notifications }
} }
enum Item: TimelineLikeCollectionViewItem { enum Item: TimelineLikeCollectionViewItem {
case group(NotificationGroup, CollapseState?, FilterState?) case group(NotificationGroup)
case loadingIndicator case loadingIndicator
case confirmLoadMore case confirmLoadMore
static func fromTimelineItem(_ item: NotificationGroup) -> Self { static func fromTimelineItem(_ item: NotificationGroup) -> Self {
switch item.kind { return .group(item)
case .mention, .status:
return .group(item, .unknown, .unknown)
default:
return .group(item, nil, nil)
}
}
static func ==(lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
case (.group(let a, _, _), .group(let b, _, _)):
return a == b
case (.loadingIndicator, .loadingIndicator):
return true
case (.confirmLoadMore, .confirmLoadMore):
return true
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case .group(let group, _, _):
hasher.combine(0)
hasher.combine(group)
case .loadingIndicator:
hasher.combine(1)
case .confirmLoadMore:
hasher.combine(1)
}
} }
var group: NotificationGroup? { var group: NotificationGroup? {
if case .group(let group, _, _) = self { if case .group(let group) = self {
return group return group
} else { } else {
return nil return nil
@ -344,7 +253,7 @@ extension NotificationsCollectionViewController {
var isSelectable: Bool { var isSelectable: Bool {
switch self { switch self {
case .group(_, _, _): case .group(_):
return true return true
default: default:
return false return false
@ -460,7 +369,7 @@ extension NotificationsCollectionViewController {
$0.id == topID $0.id == topID
} }
}! }!
if let newTopIndexPath = dataSource.indexPath(for: .group(newTopGroup, nil, nil)) { if let newTopIndexPath = dataSource.indexPath(for: .group(newTopGroup)) {
collectionView.scrollToItem(at: newTopIndexPath, at: .top, animated: false) collectionView.scrollToItem(at: newTopIndexPath, at: .top, animated: false)
} }
} }
@ -520,24 +429,14 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
} }
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath), guard case .group(let group) = dataSource.itemIdentifier(for: indexPath) else {
case .group(let group, let collapseState, let filterState) = item else {
return return
} }
switch group.kind { switch group.kind {
case .mention, .status, .poll, .update: case .mention, .status, .poll, .update:
if let filterState, let statusID = group.notifications.first!.status!.id
filterState.isWarning == true { let state = group.statusState?.copy() ?? .unknown
filterer.setResult(.allow, for: filterState) selected(status: statusID, state: state)
collectionView.deselectItem(at: indexPath, animated: true)
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems([item])
dataSource.apply(snapshot, animatingDifferences: true)
} else {
let statusID = group.notifications.first!.status!.id
let state = collapseState?.copy() ?? .unknown
selected(status: statusID, state: state)
}
case .favourite, .reblog: case .favourite, .reblog:
let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog let type = group.kind == .favourite ? StatusActionAccountListViewController.ActionType.favorite : .reblog
let statusID = group.notifications.first!.status!.id let statusID = group.notifications.first!.status!.id
@ -564,7 +463,7 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
} }
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard case .group(let group, let collapseState, _) = dataSource.itemIdentifier(for: indexPath), guard case .group(let group) = dataSource.itemIdentifier(for: indexPath),
let cell = collectionView.cellForItem(at: indexPath) else { let cell = collectionView.cellForItem(at: indexPath) else {
return nil return nil
} }
@ -574,7 +473,7 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
let status = mastodonController.persistentContainer.status(for: statusID) else { let status = mastodonController.persistentContainer.status(for: statusID) else {
return nil return nil
} }
let state = collapseState?.copy() ?? .unknown let state = group.statusState?.copy() ?? .unknown
return UIContextMenuConfiguration { return UIContextMenuConfiguration {
ConversationViewController(for: statusID, state: state, mastodonController: self.mastodonController) ConversationViewController(for: statusID, state: state, mastodonController: self.mastodonController)
} actionProvider: { _ in } actionProvider: { _ in
@ -637,7 +536,7 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
extension NotificationsCollectionViewController: UICollectionViewDragDelegate { extension NotificationsCollectionViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard case .group(let group, _, _) = dataSource.itemIdentifier(for: indexPath) else { guard case .group(let group) = dataSource.itemIdentifier(for: indexPath) else {
return [] return []
} }
switch group.kind { switch group.kind {
@ -687,13 +586,5 @@ extension NotificationsCollectionViewController: StatusCollectionViewCellDelegat
} }
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) { func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
if let indexPath = collectionView.indexPath(for: cell),
let item = dataSource.itemIdentifier(for: indexPath),
case .group(_, _, .some(let filterState)) = item {
filterer.setResult(.allow, for: filterState)
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems([item])
dataSource.apply(snapshot, animatingDifferences: true)
}
} }
} }

View File

@ -41,7 +41,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
self.mastodonController = owner.mastodonController self.mastodonController = owner.mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .account) self.filterer = Filterer(mastodonController: mastodonController, context: .account)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -24,18 +24,21 @@ class MultiSourceEmojiLabel: UILabel, BaseEmojiLabel {
var attributedStrings = pairs.map { NSAttributedString(string: $0.0) } var attributedStrings = pairs.map { NSAttributedString(string: $0.0) }
let recombine = { [weak self] in func recombine() {
if let self, if let combiner = self.combiner {
let combiner = self.combiner {
self.attributedText = combiner(attributedStrings) self.attributedText = combiner(attributedStrings)
} }
} }
recombine() recombine()
for (index, (string, emojis)) in pairs.enumerated() { for (index, (string, emojis)) in pairs.enumerated() {
self.replaceEmojis(in: string, emojis: emojis, identifier: identifier) { (attributedString, _) in self.replaceEmojis(in: string, emojis: emojis, identifier: identifier) { (attributedString, _) in
attributedStrings[index] = attributedString attributedStrings[index] = attributedString
DispatchQueue.main.async(execute: recombine) DispatchQueue.main.async { [weak self] in
guard let self else { return }
recombine()
}
} }
} }
} }

View File

@ -119,6 +119,12 @@ extension StatusCollectionViewCell {
favoriteButton.isEnabled = mastodonController.loggedIn favoriteButton.isEnabled = mastodonController.loggedIn
let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight) let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
// let didResolve = statusState.resolveFor(status: status) {
//// // layout so that we can take the content height into consideration when deciding whether to collapse
//// layoutIfNeeded()
//// return contentContainer.visibleSubviewHeight
// return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: )
// }
if didResolve { if didResolve {
if statusState.collapsible! && showStatusAutomatically { if statusState.collapsible! && showStatusAutomatically {
statusState.collapsed = false statusState.collapsed = false