Compare commits
No commits in common. "361ce456cf95972a967e35ed1a3eaae44c0d26f1" and "fc7e7f502be7c61dc02a584597dee86ea62aa026" have entirely different histories.
361ce456cf
...
fc7e7f502b
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,15 +1,6 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2023.5 (94)
|
## 2023.5 (90)
|
||||||
Features/Improvements:
|
|
||||||
- Apply filters to Notifications screen
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix editing posts not working on Akkoma
|
|
||||||
- Fix editing Markdown/HTML posts
|
|
||||||
- Fix crash when editing filter with Hide action
|
|
||||||
|
|
||||||
## 2023.5 (91)
|
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Improve performance when scrolling through timeline
|
- Improve performance when scrolling through timeline
|
||||||
- Improve error messages when editing filters
|
- Improve error messages when editing filters
|
||||||
|
|
|
@ -72,7 +72,7 @@ public class InstanceFeatures: ObservableObject {
|
||||||
|
|
||||||
public var probablySupportsMarkdown: Bool {
|
public var probablySupportsMarkdown: Bool {
|
||||||
switch instanceType {
|
switch instanceType {
|
||||||
case .pleroma(_), .mastodon(.glitch, _), .mastodon(.hometown(_), _), .calckey(_):
|
case .pleroma(_), .mastodon(.glitch, _), .mastodon(.hometown(_), _):
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -2378,7 +2378,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2444,7 +2444,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2470,7 +2470,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2499,7 +2499,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2528,7 +2528,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
|
@ -2683,7 +2683,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2714,7 +2714,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2820,7 +2820,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2846,7 +2846,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 94;
|
CURRENT_PROJECT_VERSION = 91;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
|
|
@ -126,14 +126,8 @@ class Filterer {
|
||||||
break
|
break
|
||||||
} else if old.0.pattern != new.0.pattern {
|
} else if old.0.pattern != new.0.pattern {
|
||||||
allMatch = false
|
allMatch = false
|
||||||
if new.1 == .hide {
|
// continue because we want to know if any actions changed
|
||||||
// if the pattern's changed and the action is hide, then the cell type for existing items may change
|
continue
|
||||||
actionsChanged = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
// continue because we want to know if any actions changed
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue