Compare commits
17 Commits
cced930549
...
e522e30ce5
Author | SHA1 | Date |
---|---|---|
Shadowfacts | e522e30ce5 | |
Shadowfacts | c73784aa81 | |
Shadowfacts | 7affa09e5e | |
Shadowfacts | 7435d02f6e | |
Shadowfacts | 2467297f04 | |
Shadowfacts | cf317e15e9 | |
Shadowfacts | bcae60316b | |
Shadowfacts | 1a2fa10708 | |
Shadowfacts | f79c2feea6 | |
Shadowfacts | 7ec87d7853 | |
Shadowfacts | f5704e561b | |
Shadowfacts | d6faf3a37b | |
Shadowfacts | b0a6952643 | |
Shadowfacts | 06b58cfb9c | |
Shadowfacts | afcec24f86 | |
Shadowfacts | 3f90a0df04 | |
Shadowfacts | 395ce6523d |
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,5 +1,23 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2024.1 (116)
|
||||||
|
Features/Improvements:
|
||||||
|
- Display message on empty list timelines
|
||||||
|
- Add preference to display badge for attachments that lack alt text
|
||||||
|
- Mark notifications as read on the Mastodon web frontend once displayed
|
||||||
|
- iPadOS: Support tapping the selected sidebar item to scroll to top
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix playing back GIFVs preventing the device sleeping
|
||||||
|
- Fix incorrect cell separator insets followers/following lists
|
||||||
|
- Fix memory leak in attachments gallery
|
||||||
|
- Fix notifications tab not scrolling to top when tab bar item tapped
|
||||||
|
- Fix Trending Hashtags screen not clearing selection
|
||||||
|
- Fix fast account switcher overlapping sensor housing on landscape iPhones
|
||||||
|
- Fix Edit List screen not updating when accounts are added/removed
|
||||||
|
- Fix changing List reply policy not refreshing list timeline
|
||||||
|
- macOS: Fix certain gallery attachments being incorrectly sized/positioned
|
||||||
|
|
||||||
## 2024.1 (115)
|
## 2024.1 (115)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Rewrite attachment gallery
|
- Rewrite attachment gallery
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public protocol GalleryContentViewControllerContainer {
|
public protocol GalleryContentViewControllerContainer: AnyObject {
|
||||||
var galleryControlsVisible: Bool { get }
|
var galleryControlsVisible: Bool { get }
|
||||||
|
|
||||||
func setGalleryContentLoading(_ loading: Bool)
|
func setGalleryContentLoading(_ loading: Bool)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
@MainActor
|
@MainActor
|
||||||
class GalleryDismissInteraction: NSObject {
|
class GalleryDismissInteraction: NSObject {
|
||||||
|
|
||||||
private let viewController: GalleryViewController
|
private unowned let viewController: GalleryViewController
|
||||||
|
|
||||||
private var content: GalleryContentViewController?
|
private var content: GalleryContentViewController?
|
||||||
private var origContentFrameInGallery: CGRect?
|
private var origContentFrameInGallery: CGRect?
|
||||||
|
|
|
@ -43,6 +43,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
private(set) var controlsVisible: Bool = true
|
private(set) var controlsVisible: Bool = true
|
||||||
private(set) var scrollAndZoomEnabled = true
|
private(set) var scrollAndZoomEnabled = true
|
||||||
|
|
||||||
|
private var scrollViewSizeForLastZoomScaleUpdate: CGSize?
|
||||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||||
return !controlsVisible
|
return !controlsVisible
|
||||||
}
|
}
|
||||||
|
@ -219,6 +220,12 @@ class GalleryItemViewController: UIViewController {
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
// When the scrollView size changes, make sure the zoom scale is up-to-date since it depends on the scrollView's bounds.
|
||||||
|
// This might also fix an issue on macOS (Designed for iPad) where the content isn't placed correctly. See #446
|
||||||
|
if scrollViewSizeForLastZoomScaleUpdate != scrollView.bounds.size {
|
||||||
|
scrollViewSizeForLastZoomScaleUpdate = scrollView.bounds.size
|
||||||
|
updateZoomScale(resetZoom: true)
|
||||||
|
}
|
||||||
centerContent()
|
centerContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,6 +310,8 @@ class GalleryItemViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateZoomScale(resetZoom: Bool) {
|
func updateZoomScale(resetZoom: Bool) {
|
||||||
|
scrollView.contentSize = content.contentSize
|
||||||
|
|
||||||
guard scrollAndZoomEnabled else {
|
guard scrollAndZoomEnabled else {
|
||||||
scrollView.maximumZoomScale = 1
|
scrollView.maximumZoomScale = 1
|
||||||
scrollView.minimumZoomScale = 1
|
scrollView.minimumZoomScale = 1
|
||||||
|
|
|
@ -7,34 +7,83 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct TimelineMarkers: Decodable, Sendable {
|
public struct TimelineMarkers {
|
||||||
public let home: Marker?
|
private init() {}
|
||||||
public let notifications: Marker?
|
|
||||||
|
|
||||||
public static func request(timelines: [Timeline]) -> Request<TimelineMarkers> {
|
public static func request<T: TimelineMarkerType>(timeline: T) -> Request<TimelineMarker<T.Payload>> {
|
||||||
return Request(method: .get, path: "/api/v1/markers", queryParameters: "timeline[]" => timelines.map(\.rawValue))
|
Request(method: .get, path: "/api/v1/markers", queryParameters: ["timeline[]" => T.name])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update(timeline: Timeline, lastReadID: String) -> Request<Empty> {
|
public static func update<T: TimelineMarkerType>(timeline: T, lastReadID: String) -> Request<Empty> {
|
||||||
return Request(method: .post, path: "/api/v1/markers", body: ParametersBody([
|
Request(method: .post, path: "/api/v1/markers", body: ParametersBody([
|
||||||
"\(timeline.rawValue)[last_read_id]" => lastReadID,
|
"\(T.name)[last_read_id]" => lastReadID
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Timeline: String {
|
public struct TimelineMarker<Payload: TimelineMarkerTypePayload>: Decodable, Sendable {
|
||||||
case home
|
let payload: Payload
|
||||||
case notifications
|
|
||||||
|
public var lastReadID: String {
|
||||||
|
payload.payload.lastReadID
|
||||||
|
}
|
||||||
|
public var version: Int {
|
||||||
|
payload.payload.version
|
||||||
|
}
|
||||||
|
public var updatedAt: Date {
|
||||||
|
payload.payload.updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Marker: Decodable, Sendable {
|
public init(from decoder: any Decoder) throws {
|
||||||
public let lastReadID: String
|
self.payload = try Payload(from: decoder)
|
||||||
public let version: Int
|
|
||||||
public let updatedAt: Date
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case lastReadID = "last_read_id"
|
|
||||||
case version
|
|
||||||
case updatedAt = "updated_at"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol TimelineMarkerTypePayload: Decodable, Sendable {
|
||||||
|
var payload: MarkerPayload { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct HomeMarkerPayload: TimelineMarkerTypePayload {
|
||||||
|
public var home: MarkerPayload
|
||||||
|
public var payload: MarkerPayload { home }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NotificationsMarkerPayload: TimelineMarkerTypePayload {
|
||||||
|
public var notifications: MarkerPayload
|
||||||
|
public var payload: MarkerPayload { notifications }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct MarkerPayload: Decodable, Sendable {
|
||||||
|
public let lastReadID: String
|
||||||
|
public let version: Int
|
||||||
|
public let updatedAt: Date
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case lastReadID = "last_read_id"
|
||||||
|
case version
|
||||||
|
case updatedAt = "updated_at"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol TimelineMarkerType {
|
||||||
|
static var name: String { get }
|
||||||
|
associatedtype Payload: TimelineMarkerTypePayload
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineMarkerType where Self == HomeMarker {
|
||||||
|
public static var home: Self { .init() }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineMarkerType where Self == NotificationsMarker {
|
||||||
|
public static var notifications: Self { .init() }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct HomeMarker: TimelineMarkerType {
|
||||||
|
public typealias Payload = HomeMarkerPayload
|
||||||
|
public static var name: String { "home" }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NotificationsMarker: TimelineMarkerType {
|
||||||
|
public typealias Payload = NotificationsMarkerPayload
|
||||||
|
public static var name: String { "notifications" }
|
||||||
|
}
|
||||||
|
|
|
@ -87,6 +87,7 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
||||||
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
||||||
self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true
|
self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true
|
||||||
|
self.attachmentAltBadgeInverted = try container.decodeIfPresent(Bool.self, forKey: .attachmentAltBadgeInverted) ?? false
|
||||||
|
|
||||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||||
|
@ -145,6 +146,7 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
||||||
try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline)
|
try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline)
|
||||||
try container.encode(showAttachmentBadges, forKey: .showAttachmentBadges)
|
try container.encode(showAttachmentBadges, forKey: .showAttachmentBadges)
|
||||||
|
try container.encode(attachmentAltBadgeInverted, forKey: .attachmentAltBadgeInverted)
|
||||||
|
|
||||||
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
||||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
||||||
|
@ -211,6 +213,7 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
@Published public var automaticallyPlayGifs = true
|
@Published public var automaticallyPlayGifs = true
|
||||||
@Published public var showUncroppedMediaInline = true
|
@Published public var showUncroppedMediaInline = true
|
||||||
@Published public var showAttachmentBadges = true
|
@Published public var showAttachmentBadges = true
|
||||||
|
@Published public var attachmentAltBadgeInverted = false
|
||||||
|
|
||||||
// MARK: Behavior
|
// MARK: Behavior
|
||||||
@Published public var openLinksInApps = true
|
@Published public var openLinksInApps = true
|
||||||
|
@ -274,6 +277,7 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
case automaticallyPlayGifs
|
case automaticallyPlayGifs
|
||||||
case showUncroppedMediaInline
|
case showUncroppedMediaInline
|
||||||
case showAttachmentBadges
|
case showAttachmentBadges
|
||||||
|
case attachmentAltBadgeInverted
|
||||||
|
|
||||||
case openLinksInApps
|
case openLinksInApps
|
||||||
case useInAppSafari
|
case useInAppSafari
|
||||||
|
|
|
@ -52,6 +52,12 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
config.topSeparatorVisibility = .hidden
|
config.topSeparatorVisibility = .hidden
|
||||||
config.bottomSeparatorVisibility = .hidden
|
config.bottomSeparatorVisibility = .hidden
|
||||||
}
|
}
|
||||||
|
if config.topSeparatorInsets != .zero {
|
||||||
|
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
|
}
|
||||||
|
if config.bottomSeparatorInsets != .zero {
|
||||||
|
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
|
|
|
@ -33,6 +33,8 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
config.backgroundColor = .appGroupedBackground
|
config.backgroundColor = .appGroupedBackground
|
||||||
|
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
|
config.separatorConfiguration.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
section.readableContentInset(in: environment)
|
||||||
|
|
|
@ -11,11 +11,11 @@ import Pachyderm
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class TrendingHashtagsViewController: UIViewController {
|
class TrendingHashtagsViewController: UIViewController, CollectionViewController {
|
||||||
|
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
private(set) var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var state = State.unloaded
|
private var state = State.unloaded
|
||||||
|
@ -84,6 +84,8 @@ class TrendingHashtagsViewController: UIViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
clearSelectionOnAppear(animated: animated)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await loadInitial()
|
await loadInitial()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -32,7 +32,6 @@
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="top" secondItem="1Gd-Da-Vab" secondAttribute="topMargin" placeholder="YES" id="KQs-d5-U3f"/>
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="top" secondItem="1Gd-Da-Vab" secondAttribute="topMargin" placeholder="YES" id="KQs-d5-U3f"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="lYU-Bb-3Wi" secondAttribute="trailingMargin" constant="8" id="UZh-xR-XVt"/>
|
|
||||||
<constraint firstAttribute="bottomMargin" secondItem="lYU-Bb-3Wi" secondAttribute="bottom" placeholder="YES" id="j6f-r5-NNI"/>
|
<constraint firstAttribute="bottomMargin" secondItem="lYU-Bb-3Wi" secondAttribute="bottom" placeholder="YES" id="j6f-r5-NNI"/>
|
||||||
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="leading" secondItem="1Gd-Da-Vab" secondAttribute="leading" constant="8" id="sae-ga-MGE"/>
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="leading" secondItem="1Gd-Da-Vab" secondAttribute="leading" constant="8" id="sae-ga-MGE"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
|
@ -47,6 +46,7 @@
|
||||||
<constraint firstItem="5fd-Ni-Owc" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="phf-PC-bdH"/>
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="phf-PC-bdH"/>
|
||||||
<constraint firstItem="5fd-Ni-Owc" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="rz7-cQ-PIC"/>
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="rz7-cQ-PIC"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="5fd-Ni-Owc" secondAttribute="bottom" id="sHl-iD-kGi"/>
|
<constraint firstAttribute="bottom" secondItem="5fd-Ni-Owc" secondAttribute="bottom" id="sHl-iD-kGi"/>
|
||||||
|
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="lYU-Bb-3Wi" secondAttribute="trailing" constant="8" id="snX-cx-wq6"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<point key="canvasLocation" x="140.57971014492756" y="144.64285714285714"/>
|
<point key="canvasLocation" x="140.57971014492756" y="144.64285714285714"/>
|
||||||
</view>
|
</view>
|
||||||
|
|
|
@ -70,7 +70,7 @@ class FallbackGalleryNavigationController: UINavigationController, GalleryConten
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
.zero
|
.zero
|
||||||
|
|
|
@ -59,7 +59,7 @@ class GifvGalleryContentViewController: UIViewController, GalleryContentViewCont
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
controller.item.presentationSize
|
controller.item.presentationSize
|
||||||
|
|
|
@ -114,7 +114,7 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
image.size
|
image.size
|
||||||
|
|
|
@ -13,7 +13,7 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
||||||
private let provider: () async -> (any GalleryContentViewController)?
|
private let provider: () async -> (any GalleryContentViewController)?
|
||||||
private var wrapped: (any GalleryContentViewController)!
|
private var wrapped: (any GalleryContentViewController)!
|
||||||
|
|
||||||
var container: GalleryContentViewControllerContainer?
|
weak var container: GalleryContentViewControllerContainer?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
wrapped?.contentSize ?? .zero
|
wrapped?.contentSize ?? .zero
|
||||||
|
|
|
@ -132,7 +132,7 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
item.presentationSize
|
item.presentationSize
|
||||||
|
|
|
@ -17,7 +17,7 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
||||||
|
|
||||||
private var state = State.unloaded
|
private var state = State.unloaded
|
||||||
|
|
||||||
private(set) var changedAccounts = false
|
private(set) var shouldReloadListTimeline = false
|
||||||
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
var collectionView: UICollectionView! { view as? UICollectionView }
|
var collectionView: UICollectionView! { view as? UICollectionView }
|
||||||
|
@ -186,7 +186,7 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadAccounts() async {
|
private func loadAccounts() async {
|
||||||
guard state == .unloaded else { return }
|
guard state == .unloaded || state == .loaded else { return }
|
||||||
|
|
||||||
state = .loading
|
state = .loading
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addAccount(id: String) async {
|
private func addAccount(id: String) async {
|
||||||
changedAccounts = true
|
shouldReloadListTimeline = true
|
||||||
do {
|
do {
|
||||||
let req = List.add(list.id, accounts: [id])
|
let req = List.add(list.id, accounts: [id])
|
||||||
_ = try await mastodonController.run(req)
|
_ = try await mastodonController.run(req)
|
||||||
|
@ -294,11 +294,19 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeAccount(id: String) async {
|
private func removeAccount(id: String) async {
|
||||||
changedAccounts = true
|
shouldReloadListTimeline = true
|
||||||
do {
|
do {
|
||||||
let request = List.remove(list.id, accounts: [id])
|
let request = List.remove(list.id, accounts: [id])
|
||||||
_ = try await mastodonController.run(request)
|
_ = try await mastodonController.run(request)
|
||||||
await self.loadAccounts()
|
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.itemIdentifiers.contains(.account(id: id)) {
|
||||||
|
snapshot.deleteItems([.account(id: id)])
|
||||||
|
await MainActor.run {
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
let config = ToastConfiguration(from: error, with: "Error Removing Account", in: self) { [unowned self] toast in
|
let config = ToastConfiguration(from: error, with: "Error Removing Account", in: self) { [unowned self] toast in
|
||||||
toast.dismissToast(animated: true)
|
toast.dismissToast(animated: true)
|
||||||
|
@ -309,6 +317,7 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setReplyPolicy(_ replyPolicy: List.ReplyPolicy) {
|
private func setReplyPolicy(_ replyPolicy: List.ReplyPolicy) {
|
||||||
|
shouldReloadListTimeline = true
|
||||||
Task {
|
Task {
|
||||||
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
|
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
|
||||||
await service.run(replyPolicy: replyPolicy)
|
await service.run(replyPolicy: replyPolicy)
|
||||||
|
|
|
@ -16,6 +16,8 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
var presentEditOnAppear = false
|
var presentEditOnAppear = false
|
||||||
|
|
||||||
|
private var noContentView: UIStackView!
|
||||||
|
|
||||||
private var listRenamedCancellable: AnyCancellable?
|
private var listRenamedCancellable: AnyCancellable?
|
||||||
|
|
||||||
init(for list: List, mastodonController: MastodonController) {
|
init(for list: List, mastodonController: MastodonController) {
|
||||||
|
@ -53,6 +55,39 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createNoContentView() {
|
||||||
|
let title = UILabel()
|
||||||
|
title.textColor = .secondaryLabel
|
||||||
|
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||||
|
title.adjustsFontForContentSizeCategory = true
|
||||||
|
title.text = "No Posts"
|
||||||
|
|
||||||
|
let subtitle = UILabel()
|
||||||
|
subtitle.textColor = .secondaryLabel
|
||||||
|
subtitle.font = .preferredFont(forTextStyle: .body)
|
||||||
|
subtitle.adjustsFontForContentSizeCategory = true
|
||||||
|
subtitle.numberOfLines = 0
|
||||||
|
subtitle.textAlignment = .center
|
||||||
|
subtitle.text = "This list doesn't currently have any posts. Edit it to add accounts if necessary."
|
||||||
|
|
||||||
|
noContentView = UIStackView(arrangedSubviews: [
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
])
|
||||||
|
noContentView.axis = .vertical
|
||||||
|
noContentView.spacing = 8
|
||||||
|
noContentView.alignment = .center
|
||||||
|
noContentView.isAccessibilityElement = true
|
||||||
|
noContentView.accessibilityLabel = subtitle.text
|
||||||
|
noContentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(noContentView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
noContentView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
noContentView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
|
||||||
|
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: noContentView.trailingAnchor, multiplier: 1),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
private func listChanged() {
|
private func listChanged() {
|
||||||
title = list.title
|
title = list.title
|
||||||
}
|
}
|
||||||
|
@ -61,10 +96,23 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
|
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
|
||||||
editListAccountsController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
|
editListAccountsController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
|
||||||
let navController = UINavigationController(rootViewController: editListAccountsController)
|
let navController = UINavigationController(rootViewController: editListAccountsController)
|
||||||
|
navController.sheetPresentationController?.delegate = self
|
||||||
present(navController, animated: animated)
|
present(navController, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
private func reloadIfNecessary(editViewController: EditListAccountsViewController) {
|
||||||
|
guard editViewController.shouldReloadListTimeline else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
noContentView?.removeFromSuperview()
|
||||||
|
noContentView = nil
|
||||||
|
Task {
|
||||||
|
applyInitialSnapshot()
|
||||||
|
await controller.loadInitial()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Interaction
|
||||||
|
|
||||||
@objc func editListButtonPressed() {
|
@objc func editListButtonPressed() {
|
||||||
presentEdit(animated: true)
|
presentEdit(animated: true)
|
||||||
|
@ -75,12 +123,28 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
|
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
|
|
||||||
if presented?.changedAccounts == true {
|
if let presented {
|
||||||
Task {
|
self.reloadIfNecessary(editViewController: presented)
|
||||||
applyInitialSnapshot()
|
|
||||||
await controller.loadInitial()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: TimelineLikeControllerDelegate
|
||||||
|
|
||||||
|
override func handleReplaceAllItems(_ timelineItems: [String]) async {
|
||||||
|
if timelineItems.isEmpty {
|
||||||
|
createNoContentView()
|
||||||
|
}
|
||||||
|
await super.handleReplaceAllItems(timelineItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ListTimelineViewController: UISheetPresentationControllerDelegate {
|
||||||
|
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
guard let nav = presentationController.presentedViewController as? UINavigationController,
|
||||||
|
let edit = nav.viewControllers.first as? EditListAccountsViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reloadIfNecessary(editViewController: edit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ protocol MainSidebarViewControllerDelegate: AnyObject {
|
||||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController)
|
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController)
|
||||||
|
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainSidebarViewController: UIViewController {
|
class MainSidebarViewController: UIViewController {
|
||||||
|
@ -451,7 +452,9 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
itemLastSelectedTimestamps[item] = Date()
|
itemLastSelectedTimestamps[item] = Date()
|
||||||
if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
|
if previouslySelectedItem == item {
|
||||||
|
sidebarDelegate?.sidebar(self, scrollToTopFor: item)
|
||||||
|
} else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
|
||||||
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
|
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
|
||||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
||||||
}
|
}
|
||||||
|
|
|
@ -478,6 +478,10 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||||
}
|
}
|
||||||
secondaryNavController.viewControllers = [viewController]
|
secondaryNavController.viewControllers = [viewController]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) {
|
||||||
|
(secondaryNavController as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension MainSidebarViewController.Item {
|
fileprivate extension MainSidebarViewController.Item {
|
||||||
|
|
|
@ -241,6 +241,7 @@ extension MainTabBarViewController {
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
||||||
|
@ -249,6 +250,10 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
||||||
|
|
||||||
|
// The safe area insets don't automatically propagate for some reason, so do it ourselves.
|
||||||
|
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,9 @@ import Combine
|
||||||
#if canImport(Sentry)
|
#if canImport(Sentry)
|
||||||
import Sentry
|
import Sentry
|
||||||
#endif
|
#endif
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationsCVC")
|
||||||
|
|
||||||
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController {
|
||||||
|
|
||||||
|
@ -32,6 +35,9 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
|
var updatesNotificationsMarker: Bool = false
|
||||||
|
private var newestDisplayedNotification: Item?
|
||||||
|
|
||||||
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
||||||
self.allowedTypes = allowedTypes
|
self.allowedTypes = allowedTypes
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -205,6 +211,12 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
updateNotificationsMarkerIfNecessary()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func refresh() {
|
@objc func refresh() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
if case .notLoadedInitial = controller.state {
|
if case .notLoadedInitial = controller.state {
|
||||||
|
@ -311,6 +323,23 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
await apply(snapshot, animatingDifferences: true)
|
await apply(snapshot, animatingDifferences: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateNotificationsMarkerIfNecessary() {
|
||||||
|
guard updatesNotificationsMarker,
|
||||||
|
case let .group(group, _, _) = newestDisplayedNotification,
|
||||||
|
let notification = group.notifications.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.debug("Updating notifications marker with \(notification.id)")
|
||||||
|
Task {
|
||||||
|
let req = TimelineMarkers.update(timeline: .notifications, lastReadID: notification.id)
|
||||||
|
do {
|
||||||
|
_ = try await mastodonController.run(req)
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to update notifications marker: \(String(describing: error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsCollectionViewController {
|
extension NotificationsCollectionViewController {
|
||||||
|
@ -545,6 +574,21 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
|
||||||
guard case .notifications = dataSource.sectionIdentifier(for: indexPath.section) else {
|
guard case .notifications = dataSource.sectionIdentifier(for: indexPath.section) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if updatesNotificationsMarker {
|
||||||
|
let shouldUpdateNewestDisplayedNotification: Bool
|
||||||
|
if let newestDisplayedNotification,
|
||||||
|
let currentNewestIndexPath = dataSource.indexPath(for: newestDisplayedNotification) {
|
||||||
|
shouldUpdateNewestDisplayedNotification = indexPath < currentNewestIndexPath
|
||||||
|
} else {
|
||||||
|
shouldUpdateNewestDisplayedNotification = true
|
||||||
|
}
|
||||||
|
if shouldUpdateNewestDisplayedNotification,
|
||||||
|
let item = dataSource.itemIdentifier(for: indexPath) {
|
||||||
|
newestDisplayedNotification = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
|
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
|
||||||
if indexPath.row == itemsInSection - 1 {
|
if indexPath.row == itemsInSection - 1 {
|
||||||
Task {
|
Task {
|
||||||
|
@ -752,3 +796,22 @@ extension NotificationsCollectionViewController: StatusCollectionViewCellDelegat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationsCollectionViewController: TabBarScrollableViewController {
|
||||||
|
func tabBarScrollToTop() {
|
||||||
|
collectionView.scrollToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationsCollectionViewController: StatusBarTappableViewController {
|
||||||
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
|
collectionView.scrollToTop()
|
||||||
|
return .stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationsCollectionViewController: BackgroundableViewController {
|
||||||
|
func sceneDidEnterBackground() {
|
||||||
|
updateNotificationsMarkerIfNecessary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
|
||||||
let vc = NotificationsCollectionViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController)
|
let vc = NotificationsCollectionViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController)
|
||||||
vc.title = page.title
|
vc.title = page.title
|
||||||
vc.userActivity = page.userActivity(accountID: mastodonController.accountInfo!.id)
|
vc.userActivity = page.userActivity(accountID: mastodonController.accountInfo!.id)
|
||||||
|
vc.updatesNotificationsMarker = page == .all
|
||||||
return vc
|
return vc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,13 @@ struct MediaPrefsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $preferences.showAttachmentBadges) {
|
Toggle(isOn: $preferences.showAttachmentBadges) {
|
||||||
Text("Show GIF/ALT Badges")
|
Text("Show GIF/\(Text("Alt").font(.body.lowercaseSmallCaps())) Badges")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $preferences.attachmentAltBadgeInverted) {
|
||||||
|
Text("Show Badge when Missing \(Text("Alt").font(.body.lowercaseSmallCaps()))")
|
||||||
|
}
|
||||||
|
.disabled(!preferences.showAttachmentBadges)
|
||||||
}
|
}
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
|
@ -387,16 +387,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
_ = try await mastodonController.run(req)
|
_ = try await mastodonController.run(req)
|
||||||
} catch {
|
} catch {
|
||||||
stateRestorationLogger.error("TimelineViewController: failed to update timeline marker: \(String(describing: error))")
|
stateRestorationLogger.error("TimelineViewController: failed to update timeline marker: \(String(describing: error))")
|
||||||
|
|
||||||
#if canImport(Sentry)
|
|
||||||
if let error = error as? Client.Error,
|
|
||||||
case .networkError(_) = error.type {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let event = Event(error: error)
|
|
||||||
event.message = SentryMessage(formatted: "Failed to update timeline marker: \(String(describing: error))")
|
|
||||||
SentrySDK.capture(event: event)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -573,12 +563,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
@MainActor
|
@MainActor
|
||||||
private func restoreStatusesFromMarkerPosition() async -> Bool {
|
private func restoreStatusesFromMarkerPosition() async -> Bool {
|
||||||
do {
|
do {
|
||||||
let (marker, _) = try await mastodonController.run(TimelineMarkers.request(timelines: [.home]))
|
let (marker, _) = try await mastodonController.run(TimelineMarkers.request(timeline: .home))
|
||||||
guard let home = marker.home else {
|
async let status = try await mastodonController.run(Client.getStatus(id: marker.lastReadID)).0
|
||||||
return false
|
async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: marker.lastReadID, count: Self.pageSize))).0
|
||||||
}
|
|
||||||
async let status = try await mastodonController.run(Client.getStatus(id: home.lastReadID)).0
|
|
||||||
async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: home.lastReadID, count: Self.pageSize))).0
|
|
||||||
|
|
||||||
let allStatuses = try await [status] + olderStatuses
|
let allStatuses = try await [status] + olderStatuses
|
||||||
await mastodonController.persistentContainer.addAll(statuses: allStatuses)
|
await mastodonController.persistentContainer.addAll(statuses: allStatuses)
|
||||||
|
@ -590,21 +577,11 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
await apply(snapshot, animatingDifferences: false)
|
await apply(snapshot, animatingDifferences: false)
|
||||||
collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top)
|
collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top)
|
||||||
|
|
||||||
stateRestorationLogger.debug("TimelineViewController: restored from timeline marker with last read ID: \(home.lastReadID)")
|
stateRestorationLogger.debug("TimelineViewController: restored from timeline marker with last read ID: \(marker.lastReadID)")
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
stateRestorationLogger.error("TimelineViewController: failed to load from timeline marker: \(String(describing: error))")
|
stateRestorationLogger.error("TimelineViewController: failed to load from timeline marker: \(String(describing: error))")
|
||||||
|
|
||||||
#if canImport(Sentry)
|
|
||||||
if let error = error as? Client.Error,
|
|
||||||
case .networkError(_) = error.type {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let event = Event(error: error)
|
|
||||||
event.message = SentryMessage(formatted: "Failed to load from timeline marker: \(String(describing: error))")
|
|
||||||
SentrySDK.capture(event: event)
|
|
||||||
#endif
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1016,6 +993,18 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
self.showToast(configuration: config, animated: true)
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is copied from the TimelineLikeCollectionViewController implementation because it needs to be overridable by ListTimelineViewController
|
||||||
|
func handleReplaceAllItems(_ timelineItems: [String]) async {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.sectionIdentifiers.contains(.entries) {
|
||||||
|
snapshot.deleteSections([.entries])
|
||||||
|
}
|
||||||
|
snapshot.appendSections([.entries])
|
||||||
|
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
|
||||||
|
await apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController {
|
extension TimelineViewController {
|
||||||
|
|
|
@ -258,6 +258,12 @@ class SplitNavigationController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SplitNavigationController: TabBarScrollableViewController {
|
||||||
|
func tabBarScrollToTop() {
|
||||||
|
(viewControllers.last as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension SplitNavigationController: StatusBarTappableViewController {
|
extension SplitNavigationController: StatusBarTappableViewController {
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
let vcs = viewControllers
|
let vcs = viewControllers
|
||||||
|
|
|
@ -30,6 +30,8 @@ class AttachmentView: GIFImageView {
|
||||||
var attachment: Attachment!
|
var attachment: Attachment!
|
||||||
var index: Int!
|
var index: Int!
|
||||||
|
|
||||||
|
private var currentBadges: Badges = []
|
||||||
|
|
||||||
private var loadAttachmentTask: Task<Void, Never>?
|
private var loadAttachmentTask: Task<Void, Never>?
|
||||||
private var source: Source?
|
private var source: Source?
|
||||||
var attachmentImage: UIImage? {
|
var attachmentImage: UIImage? {
|
||||||
|
@ -103,8 +105,9 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if getBadges().isEmpty != Preferences.shared.showAttachmentBadges {
|
let newBadges = getBadges()
|
||||||
createBadgesView(getBadges())
|
if currentBadges != newBadges {
|
||||||
|
createBadgesView(newBadges)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,8 +178,13 @@ class AttachmentView: GIFImageView {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
var badges: Badges = []
|
var badges: Badges = []
|
||||||
if attachment.description?.isEmpty == false {
|
let hasDescription = attachment.description?.isEmpty == false
|
||||||
|
if hasDescription,
|
||||||
|
!Preferences.shared.attachmentAltBadgeInverted {
|
||||||
badges.formUnion(.alt)
|
badges.formUnion(.alt)
|
||||||
|
} else if !hasDescription,
|
||||||
|
Preferences.shared.attachmentAltBadgeInverted {
|
||||||
|
badges.formUnion(.noAlt)
|
||||||
}
|
}
|
||||||
if attachment.kind == .gifv || attachment.url.pathExtension == "gif" {
|
if attachment.kind == .gifv || attachment.url.pathExtension == "gif" {
|
||||||
badges.formUnion(.gif)
|
badges.formUnion(.gif)
|
||||||
|
@ -354,6 +362,8 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createBadgesView(_ badges: Badges) {
|
private func createBadgesView(_ badges: Badges) {
|
||||||
|
currentBadges = badges
|
||||||
|
|
||||||
guard !badges.isEmpty else {
|
guard !badges.isEmpty else {
|
||||||
badgeContainer?.removeFromSuperview()
|
badgeContainer?.removeFromSuperview()
|
||||||
badgeContainer = nil
|
badgeContainer = nil
|
||||||
|
@ -393,6 +403,9 @@ class AttachmentView: GIFImageView {
|
||||||
if badges.contains(.alt) {
|
if badges.contains(.alt) {
|
||||||
makeBadgeView(text: "ALT")
|
makeBadgeView(text: "ALT")
|
||||||
}
|
}
|
||||||
|
if badges.contains(.noAlt) {
|
||||||
|
makeBadgeView(text: "No ALT")
|
||||||
|
}
|
||||||
|
|
||||||
let first = stack.arrangedSubviews.first!
|
let first = stack.arrangedSubviews.first!
|
||||||
first.layer.masksToBounds = true
|
first.layer.masksToBounds = true
|
||||||
|
@ -445,6 +458,7 @@ fileprivate extension AttachmentView {
|
||||||
struct Badges: OptionSet {
|
struct Badges: OptionSet {
|
||||||
static let gif = Badges(rawValue: 1 << 0)
|
static let gif = Badges(rawValue: 1 << 0)
|
||||||
static let alt = Badges(rawValue: 1 << 1)
|
static let alt = Badges(rawValue: 1 << 1)
|
||||||
|
static let noAlt = Badges(rawValue: 1 << 2)
|
||||||
|
|
||||||
let rawValue: Int
|
let rawValue: Int
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ class GifvController {
|
||||||
self.isGrayscale = Preferences.shared.grayscaleImages
|
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
player.isMuted = true
|
player.isMuted = true
|
||||||
|
player.preventsDisplaySleepDuringVideoPlayback = false
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
MARKETING_VERSION = 2024.1
|
MARKETING_VERSION = 2024.1
|
||||||
CURRENT_PROJECT_VERSION = 115
|
CURRENT_PROJECT_VERSION = 116
|
||||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||||
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||||
|
|
Loading…
Reference in New Issue